1. 進程、線程、任務
進程(process),指的是一個正在運行中的可執行文件。每一個進程都擁有獨立的虛擬內存空間和系統資源,包括端口權限等,且至少包含一個主線程和任意數量的輔助線程。另外,當一個進程的主線程退出時,這個進程就結束了;
線程(thread),指的是一個獨立的代碼執行路徑,也就是說線程是代碼執行路徑的最小分支。在 iOS 中,線程的底層實現是基于 POSIX threads API 的,也就是我們常說的 pthreads ;
任務(task),指的是我們需要執行的工作,是一個抽象的概念,用通俗的話說,就是一段代碼。
串行 vs. 并發
從本質上來說,串行和并發的主要區別在于允許同時執行的任務數量。串行,指的是一次只能執行一個任務,必須等一個任務執行完成后才能執行下一個任務;并發,則指的是允許多個任務同時執行。
同步 vs. 異步
同樣的,同步和異步操作的主要區別在于是否等待操作執行完成,亦即是否阻塞當前任務。
同步操作會阻塞當前任務,等待操作執行完成后再繼續執行接下來的代碼,而異步操作則恰好相反,它會在調用后立即返回,不會等待操作的執行結果。
2. Operation Queues vs. Grand Central Dispatch (GCD)
簡單來說,GCD 是蘋果基于 C 語言開發的,一個用于多核編程的解決方案,主要用于優化應用程序以支持多核處理器以及其他對稱多處理系統。而 Operation Queues 則是一個建立在 GCD 的基礎之上的,面向對象的解決方案。它使用起來比 GCD 更加靈活,功能也更加強大。
Operation Queues :相對 GCD 來說,使用 Operation 和 Operation Queues 會增加一點點額外的開銷,但是我們卻換來了非常強大的靈活性和功能,我們可以給 operation 之間添加依賴關系、取消一個正在執行的 operation 、暫停和恢復 operation queue 、設置Operation優先級等;GCD也可以進行suspend/resume,但不能取消。NSOperation作為OC對象,還支持KVO,這也是GCD所沒有的。
GCD :則是一種更輕量級的,以 FIFO 的順序執行并發任務的方式,使用 GCD 時我們并不關心任務的調度情況,而讓系統幫我們自動處理。但是 GCD 的短板也是非常明顯的,比如我們想要給任務之間添加依賴關系、取消或者暫停一個正在執行的任務時就會變得非常棘手。
NSOperation,被添加到隊列中后-(void)start
方法會被調用,方法內會檢查和設置op的狀態,之后調用-(void)main
方法。如果一個op不打算放入隊列,也可以手動調用開始方法,但是對于已經在隊列中的op,再手動調用開始方法是錯誤的。
GCD使用注意事項
2.1 dispatch_once_t必須是全局或static變量,非全局或非static的dispatch_once_t變量在使用時可能會導致非常不好排查的bug。
2.2 創建隊列dispatch_queue_t dispatch_queue_create ( const char *label, dispatch_queue_attr_t attr );
第二個參數dispatch_queue_attr_t在網上教程中常用NULL,實際提供了更清晰、嚴謹的參數DISPATCH_QUEUE_SERIAL、DISPATCH_QUEUE_CONCURRENT
2.3 dispatch_after是延遲提交,而非延遲運行。需要延遲運行可以使用定時器。
2.4 dispatch_suspend并非立即停止隊列的運行,而是在當前block任務執行完成后,暫停后續的block執行。
2.5 dispatch_apply會“等待”其所有的循環運行完畢才往下執行,也就是會阻塞外部的線程。嵌套使用也會造成死鎖。
dispatch_queue_t queue = dispatch_queue_create("com.my.testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_apply(5, queue, ^(size_t i) {
NSLog(@"outter loop: %zu", i);
// 此處造成死鎖
dispatch_apply(3, queue, ^(size_t j) {
NSLog(@"inner loop: %zu", j);
});
});
2.6 dispatchbarrier\(a)sync
作用就是向某個隊列插入一個block,當目前正在執行的block運行完成后,阻塞這個block后面添加的block,只運行這個block直到完成,然后再繼續后續的任務。這個效果只在自己創建的并發隊列上有效,使用其他隊列效果則與dispatch_(a)sync
一樣。
2.7 dispatch_set_context
可以為隊列添加上下文數據,但是因為GCD是C語言接口形式的,所以其context參數類型是“void *”。如果參數用Objective-C的對象,但是要用__bridge等關鍵字轉為Core Foundation對象,同時注意在dispatch_set_finalizer_f
對應的函數中釋放,避免內存泄露。
/*
__bridge: 只做了類型轉換,不修改內存管理權;
__bridge_retained(即CFBridgingRetain)轉換類型,同時將內存管理權從ARC中移除,后面需要使用CFRelease來釋放對象;
__bridge_transfer(即CFBridgingRelease)將Core Foundation的對象轉換為Objective-C的對象,同時將內存管理權交給ARC。
*/
void cleanStaff(void *context) {
//這里用__bridge轉換,不改變內存管理權
Data *data = (__bridge Data *)(context);
NSLog(@"In clean, context number: %d", data.number);
//釋放context的內存!
CFRelease(context);
}
- (void)testBody {
//創建隊列
dispatch_queue_t queue = dispatch_queue_create("me.tutuge.test.gcd", DISPATCH_QUEUE_SERIAL);
//創建自定義Data類型context數據并初始化
Data *myData = [Data new];
myData.number = 10;
//綁定context
//這里用__bridge_retained轉換,將context的內存管理權從ARC移除,交由我們自己手動釋放!
dispatch_set_context(queue, (__bridge_retained void *)(myData));
//設置finalizer函數,用于在隊列執行完成后釋放對應context內存
dispatch_set_finalizer_f(queue, cleanStaff);
dispatch_async(queue, ^{
//獲取隊列的context數據
//這里用__bridge轉換,不改變內存管理權
Data *data = (__bridge Data *)(dispatch_get_context(queue));
//打印
NSLog(@"1: context number: %d", data.number);
//修改context保存的數據
data.number = 20;
});
}
2.8 在當前隊列中使用sync提交任務到當前隊列,造成死鎖。需要注意的是,隊列和線程并非同一個概念,每個隊列放的Block任務會在線程中執行,可能是主線程或子線程。
// 代碼在ViewDidLoad方法,即主線程中
/*
Calling 'dispatch_sync' function and targeting the current queue results in deadlock.
1. dispatch_sync將block提交到main queue
2. dispatch_sync在阻塞當前隊列任務的執行,直到Block執行完成
3. dispatch_sync死鎖,因為要執行Block的隊列被阻塞,Block無法完成
*/
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"main");
});
// 不會死鎖
/* As an optimization, this function invokes the block on the current thread when possible.
1. dispatch_sync將block提交到自定義隊列queue
2. dispatch_sync阻塞當前隊列任務的執行,直到Block執行完成
3. queue在當前線程(主線程)執行Block,log輸出主線程信息
*/
dispatch_queue_t queue = dispatch_queue_create("com.myQueue", NULL);
dispatch_sync(queue, ^{
NSLog(@"queue:%@",[NSThread currentThread]); // log main thread info
});
上面自定義隊列的例子,輸出了主線程的信息,因此網上其他博客中常說到的dispatch_sync
會阻塞當前線程的說法是錯誤的,畢竟queue中的任務在主線程執行了,因此應該是“阻塞了隊列或者隊列當前任務”更加準確。
2.9 GCD只能suspend和resume隊列,并不能cancel。
3. 多線程安全
多線程的安全,一方面是防止一個進程在寫時,另一個進程也在寫入或者讀取;另一方面是保證一個代碼片段內對內存的連續多次訪問結果一致。
線程安全是有粒度大小的,可能是一個model,可能是model中的一個數組,可能是一段代碼或一個方法。
Apple的多線程編碼文檔: Threading Programming Guide。
3.1 @synchronized
@synchronized
和其他互斥鎖一樣,它防止不同的線程在同一時間獲取相同的鎖,而且不需要使用代碼直接創建互斥鎖對象,而是直接使用OC對象作為一個lock token。@synchronized
隱式添加了異常處理代碼,如果代碼塊中拋出異常會自動釋放互斥鎖。
@synchronized(obj) {
// do work
}
// 上面代碼實際上為,objc_sync_enter會創建一個與obj關聯的互斥鎖
@try {
objc_sync_enter(obj);
// do work
} @finally {
objc_sync_exit(obj);
}
如果傳入的是nil對象,則會是一個空操作,失去了線程安全的功能,應該避免這種情況發生。
在SDWebImage中也有使用@synchronized
實現線程安全的情況:
// SDWebImageDownloaderOperation
@synchronized(self) {...}
// SDWebImageManager.m
@synchronized(self.failedURLs) {...}
@synchronized(self.runningOpeations) {...}
3.2 atomic關鍵字
atomic的作用只是給getter和setter加了個鎖,atomic只能保證代碼進入getter或者setter函數內部時是安全的,一旦出了getter和setter,多線程安全只能靠程序員自己保障了。所以atomic屬性和使用property的多線程安全并沒什么直接的聯系。另外,atomic由于加鎖也會帶來一些性能損耗,所以我們在編寫iOS代碼的時候,一般聲明property為nonatomic,在需要做多線程安全的場景,自己去額外加鎖做同步。
@property (atomic, strong) NSString* stringA;
//thread A
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.stringA = @"a very long string";
}
else {
self.stringA = @"string";
}
NSLog(@"Thread A: %@\n", self.stringA);
}
//thread B
for (int i = 0; i < 100000; i ++) {
// getter 1
if (self.stringA.length >= 10) {
// getter 2
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
NSLog(@"Thread B: %@\n", self.stringA);
}
上面代碼,在線程B中,getter2可能出現崩潰。原因是在getter1時,字符串長度大于10,而在getter2時字符串內容已經在線程A中被修改了,因此發生崩潰。
stringA屬性是原子性的,它的set/get方法都是線程安全的,但是問題發生在set/get方法之外,兩次對stringA內存區域的訪問,內存內容已經發生了改變,因此需要加鎖來保證這一段代碼是原子性的。
//thread A
[_lock lock];
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.stringA = @"a very long string";
}
else {
self.stringA = @"string";
}
NSLog(@"Thread A: %@\n", self.stringA);
}
[_lock unlock];
//thread B
[_lock lock];
if (self.stringA.length >= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
[_lock unlock];
NSLock 可以協調同一應用程序中多個線程。可以保護全局數據以原子方式訪問或代碼片段以原子方式運行。
調用NSLock的unlock方法時,必須確保和調用lock方法的線程為同一個,如果是在不同線程可能發生不可預期的效果。
不應該使用NSLock來實現遞歸鎖,在同一個線程上調用lock兩次,線程將永遠被鎖住。遞歸鎖應該使用NSRecursiveLock。
OSSpinLock會出現優先級反轉的情況,也會出現空轉耗CPU的情況,不適用于較長時間的任務,而且在iOS 10和macOS10.12標記了Deprecated(沒徹底移除)。引入了一個新的os_unfair_lock,也是忙等機制。
在使用NSThread、NSOperation等OC對象的過程中涉及到多線程安全,常常會使用NSLock、NSConditionLock、NSRecursiveLock、NSCondition等類。
3.3GCD線程安全
3.3.1 dispatch_semaphore
信號量
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(signal, overTime);
NSLog(@"需要線程同步的操作1 開始");
sleep(2);
NSLog(@"需要線程同步的操作1 結束");
dispatch_semaphore_signal(signal);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
dispatch_semaphore_wait(signal, overTime);
NSLog(@"需要線程同步的操作2");
dispatch_semaphore_signal(signal);
});
3.3.2 dispatch_barrier_(a)sync
+ 自定義并發隊列
以SDWebImage中的代碼為例
// SDImageCache.m
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
dispatch_async(self.ioQueue, ^{
// 執行緩存圖片的增刪查等操作
});
3.3.3 dispatch_sync
+ 自定義串行隊列
// SDWebImageDownloader.m
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
// 對self.URLOperations的增刪
dispatch_barrier_async(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];
BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
if (canceled) {
[self.URLOperations removeObjectForKey:token.url];
}
});
參考文章: