多線程學習 每一個iOS應用(進程)運行都會有一個主線程(UI線程),UI上的更新推薦在主線程中去完成。多線程本身並不複雜,難點在於多個線程在其生命周期的管理,如線程的執行順序、線程間的數據共用以及資源競爭等問題。 本文主要記錄開發中常用的3種多線程模式: NSThread NSOperation ...
多線程學習
每一個iOS應用(進程)運行都會有一個主線程(UI線程),UI上的更新推薦在主線程中去完成。多線程本身並不複雜,難點在於多個線程在其生命周期的管理,如線程的執行順序、線程間的數據共用以及資源競爭等問題。
本文主要記錄開發中常用的3種多線程模式:
- NSThread
- NSOperation
- GCD
一、NSThread
NSThread是一種輕量級的多線程開發模式,使用起來也比較簡單主要通過一下兩個方法來創建新線程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);
[NSThread start];
線程狀態分為3類:正在運行、已經完成、正在取消;可以用cancel來改變線程狀態,註意這並沒有真正的終止線程,除非調用其靜態方法[NSTread exist]來終止線程。
線程優先順序的範圍是0~1,值越大優先順序越高,線程預設值為0.5
NSObject分類 NSThreadPerformAdditions提供線程UI更新的主要方法如下:
//主線程才能更新UI
[self performSelectorOnMainThread:@selector(updateImageView:) withObject:imagedata waitUntilDone:YES];
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
總結
- 使用簡單,但線程的執行順序很難控制;
- 需要自己管理線程的生命周期,一個任務創建一個線程占用系統開銷;
二、NSOperation
類似C#中的線程池,創建NSOperation放入NSOperationQueue隊列中一次啟動執行。NSOperation更加容易管理線程總數和線程之間的依賴關係。
NSOperation有兩個子類用於創建線程:NSInvocationOperation 和 NSBlockOperation。
// NSInvocationOperation創建
//創建NSOperation
NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector() object:nil];
//創建操作隊列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//加入到隊列,開啟一個線程執行隊列中的線程
[queue addOperation:operation];
//NSBlockOperation創建
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//1、創建
queue.maxConcurrentOperationCount = 5;
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
}];
[queue addOperation:operation];
//2、直接代碼塊加入隊列
[queue addOperationWithBlock:^{
}];
總結
- NSOperation可以設置最大併發數目;
- 可以設置依賴線程。假設線程A依賴線程B,操作隊列啟動後就會首先執行B再執行A,[operationA addDependency:OperationB]
三、GCD(Grand Central Dispatch)
基於C語言開發的一套多線程開發機制,也是蘋果推薦的多線程開發方法。這種機制最顯著的優點就是他對多核運算更佳有效。
GCD有一個類似NSOperationQueue的隊列,分為:
- 串列隊列:只有一個線程,加入到隊列的操作依次執行。
- 並行隊列:多個線程,將操作任務安排在可用的處理器上,同時保證先進來的任務先處理。
- 主隊列:用於在主線程上執行的任務隊列,如UI更新。
//創建一個串列非同步隊列
dispatch_queue_t serialQueue = dispatch_queue_create(@"queuename", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
});
//UI更新
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
});
併發隊列同樣是使用dispatch_queue_create()方法創建,只是最後一個參數指定為DISPATCH_QUEUE_CONCURRENT進行創建,但是在實際開發中我們通常不會重新創建一個併發隊列而是使用dispatch_get_global_queue()方法取得一個全局的併發隊列。
//創建一個全局並行非同步隊列
dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(global, ^{
});
GCD其他常用的執行方法:
- dispatch_apply():重覆執行某個任務,但是註意這個方法沒有辦法非同步執行(為了不阻塞線程可以使用dispatch_async()包裝一下再執行)。
- dispatch_once():單次執行一個任務,此方法中的任務只會執行一次,重覆調用也沒辦法重覆執行(單例模式中常用此方法)。
- dispatch_time():延遲一定的時間後執行。
- dispatch_barrier_async():使用此方法創建的任務首先會查看隊列中有沒有別的任務要執行,如果有,則會等待已有任務執行完畢再執行;同時在此方法後添加的任務必須等待此方法中任務執行後才能執行。(利用這個方法可以控制執行順序,例如前面先載入最後一張圖片的需求就可以先使用這個方法將最後一張圖片載入的操作添加到隊列,然後調用dispatch_async()添加其他圖片載入任務)
- dispatch_group_async():實現對任務分組管理,如果一組任務全部完成可以通過6. dispatch_group_notify()方法獲得完成通知(需要定義dispatch_group_t作為分組標識)。
GCD的鎖機制
說到多線程就不得不提多線程中的鎖機制,多線程操作過程中往往多個線程是併發執行的,同一個資源可能被多個線程同時訪問,造成資源搶奪,這個過程中如果沒有鎖機制往往會造成重大問題。
NSLock : 同步鎖NSLock來解決,使用時把需要加鎖的代碼(以後暫時稱這段代碼為”加鎖代碼“)放到NSLock的lock和unlock之間,一個線程A進入加鎖代碼之後由於已經加鎖,另一個線程B就無法訪問,只有等待前一個線程A執行完加鎖代碼後解鎖,B線程才能訪問加鎖代碼。
@synchronized代碼塊:日常開發中也更推薦使用此方法。首先選擇一個對象作為同步對象(一般使用self),然後將”加鎖代碼”(爭奪資源的讀取、修改代碼)放到代碼塊中。@synchronized中的代碼執行時先檢查同步對象是否被另一個線程占用,如果占用該線程就會處於等待狀態,直到同步對象被釋放。
//線程同步
@synchronized(self){
if (_imageNames.count>0) {
name=[_imageNames lastObject];
[NSThread sleepForTimeInterval:0.001f];
[_imageNames removeObject:name];
}
}
GCD信號機制
在GCD中提供了一種信號機制,也可以解決資源搶占問題(和同步鎖的機制並不一樣)。GCD中信號量是dispatch_semaphore_t類型,支持 信號通知 和 信號等待。每當發送一個信號通知,則信號量+1;每當發送一個等待信號時信號量-1,;如果信號量為0則信號會處於等待狀態,直到信號量大於0開始執行。根據這個原理我們可以初始化一個信號量變數,預設信號量設置為1,每當有線程進入“加鎖代碼”之後就調用信號等待命令(此時信號量為0)開始等待,此時其他線程無法進入,執行完後發送信號通知(此時信號量為1),其他線程開始進入執行,如此一來就達到了線程同步目的。
dispatch_semaphore_t _semaphore;//定義一個信號量
//初始化信號量參數
_semaphore = dispatch_semaphore_create(1);
//信號等待,第二個參數:等待時間
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
if (_imageNames.count>0) {
name=[_imageNames lastObject];
[_imageNames removeObject:name];
}
//信號通知
dispatch_semaphore_signal(_semaphore);