iOS開發之GCD使用總結
GCD是iOS的一種底層多線程機制,今天總結一下GCD的常用API和概念,希望對大家的學習起到協助作用。 GCD隊列的概念 在多線程開發當中,程式員只要將想做的事情定義好,並追加到DispatchQueue(派發隊列)當中就好了。 派發隊列分為兩種,一種是串列隊列(SerialDispatchQueue),一種是並行隊列(ConcurrentDispatchQueue)。 一個任務就是一個block,比如,將任務添加到隊列中的代碼是: 1 dispatch_async(queue, block);當給queue添加多個任務時,如果queue是串列隊列,則它們按順序一個個執行,同時處理的任務只有一個。 當queue是並行隊列時,不論第一個任務是否結束,都會立刻開始執行後面的任務,也就是可以同時執行多個任務。 但是並存執行的任務數量取決於XNU核心,是不可控的。比如,如果同時執行10個任務,那麼10個任務並不是開啟10個線程,線程會根據任務執行情況複用,由系統控制。 擷取隊列 系統提供了兩個隊列,一個是MainDispatchQueue,一個是GlobalDispatchQueue。 前者會將任務插入主線程的RunLoop當中去執行,所以顯然是個串列隊列,我們可以使用它來更新UI。 後者則是一個全域的並行隊列,有高、預設、低和後台4個優先順序。 它們的擷取方式如下: 1 dispatch_queue_t queue = dispatch_get_main_queue();2 3 dispatch queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRORITY_DEFAULT, 0) 執行非同步任務 1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);2 dispatch_async(queue, ^{3 //...4 }); 這個程式碼片段直接在子線程裡執行了一個任務塊。使用GCD方式任務是立即開始執行的 它不像操作隊列那樣可以手動啟動,同樣,缺點也是它的不可控性。 令任務只執行一次 1 + (id)shareInstance {2 static dispatch_once_t onceToken;3 dispatch_once(&onceToken, ^{4 _shareInstance = [[self alloc] init];5 });6 }這種只執行一次且安全執行緒的方式經常出現在單例構造器當中。 工作群組 有時候,我們希望多個任務同時(在多個線程裡)執行,再他們都完成之後,再執行其他的任務, 於是可以建立一個分組,讓多個任務形成一個組,下面的代碼在組中多個任務都執行完畢之後再執行後續的任務: 複製代碼 1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 2 dispatch_group_t group = dispatch_group_create(); 3 4 dispatch_group_async(group, queue, ^{ NSLog(@"1"); }); 5 dispatch_group_async(group, queue, ^{ NSLog(@"2"); }); 6 dispatch_group_async(group, queue, ^{ NSLog(@"3"); }); 7 dispatch_group_async(group, queue, ^{ NSLog(@"4"); }); 8 dispatch_group_async(group, queue, ^{ NSLog(@"5"); }); 9 10 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"done"); });複製代碼 順延強制任務 1 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{2 //...3 });這段代碼將會在10秒後將任務插入RunLoop當中。 dispatch_asycn和dispatch_sync 先前已經有過一個使用dispatch_async執行非同步任務的一個例子,下面來看一段代碼: 複製代碼1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);2 3 dispatch_async(queue, ^{4 NSLog(@"1");5 });6 7 NSLog(@"2");複製代碼這段代碼首先擷取了全域隊列,也就是說,dispatch_async當中的任務被丟到了另一個線程裡去執行,async在這裡的含義是,噹噹前線程給子線程分配了block當中的任務之後,當前線程會立即執行,並不會發生阻塞,也就是非同步。那麼,輸出結果不是12就是21,因為我們沒法把控兩個線程RunLoop裡到底是怎麼執行的。 類似的,還有一個“同步”方法dispatch_sync,代碼如下: 複製代碼1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);2 3 dispatch_sync(queue, ^{4 NSLog(@"1");5 });6 7 NSLog(@"2");複製代碼這就意味著,當主線程將任務分給子線程後,主線程會等待子線程執行完畢,再繼續執行自身的內容,那麼結果顯然就是12了。 需要注意的一點是,這裡用的是全域隊列,那如果把dispatch_sync的隊列換成主線程隊列會怎麼樣呢: 1 dispatch_queue_t queue = dispatch_get_main_queue();2 dispatch_sync(queue, ^{3 NSLog(@"1");4 });這段代碼會發生死結,因為: 1.主線程通過dispatch_sync把block交給主隊列後,會等待block裡的任務結束再往下走自身的任務, 2.而隊列是先進先出的,block裡的任務也在等待主隊列當中排在它之前的任務都執行完了再走自己。 這種迴圈等待就形成了死結。所以在主線程當中使用dispatch_sync將任務加到主隊列是不可取的。 建立隊列 我們可以使用系統提供的函數擷取主串列隊列和全域並行隊列,當然也可以自己手動建立串列和並行隊列,代碼為: 1 dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.Steak.GCD", DISPATCH_QUEUE_SERIAL);2 dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.Steak.GCD", DISPATCH_QUEUE_CONCURRENT);在MRC下,手動建立的隊列是需要釋放的 1 dispatch_release(myConcurrentDispatchQueue);手動建立的隊列和預設優先順序全域隊列優先順序等同,如果需要修改隊列的優先順序,需要: 1 dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.Steak.GCD", DISPATCH_QUEUE_CONCURRENT);2 dispatch_queue_t targetQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);3 dispatch_set_target_queue(myConcurrentDispatchQueue, targetQueue);上面的代碼修改隊列的優先順序為後台層級,即與預設的後台優先順序的全域隊列等同。 串列、並行隊列與讀寫安全性 在向串列隊列(SerialDispatchQueue)當中加入多個block任務後,一次只能同時執行一個block,如果產生了n個串列隊列,並且向每個隊列當中都添加了任務,那麼系統就會啟動n個線程來同時執行這些任務。 對於串列隊列,正確的使用時機,是在需要解決資料/檔案競爭問題時使用它。比如,我們可以令多個任務同時訪問一塊資料,這樣會出現衝突,也可以把每個操作都加入到一個串列隊列當中,因為串列隊列一次只能執行一個線程的任務,所以不會出現衝突。 但是考慮到串列隊列會因為環境切換而拖慢系統效能,所以我們還是很期望採用並行隊列的,來看下面的範例程式碼: 複製代碼 1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 2 dispatch_async(queue, ^{ 3 //資料讀取 4 }); 5 dispatch_async(queue, ^{ 6 //資料讀取2 7 }); 8 dispatch_async(queue, ^{ 9 //資料寫入10 });11 dispatch_async(queue, ^{12 //資料讀取313 });14 dispatch_async(queue, ^{15 //資料讀取416 });複製代碼顯然,這5個操作的執行順序是我們無法預期的,我們希望在讀取1和讀取2執行結束後,再執行寫入,寫入完成後再執行讀取3和讀取4。 為了實現這個效果,這裡可以使用GCD的另一個API: 1 dispatch_barrier_async(queue, ^{2 //資料寫入3 });這樣就保證的寫入操作的並發安全性。 對於沒有資料競爭的並行操作,則可以使用並行隊列(CONCURRENT)來實現。 JOIN行為 CGD利用dispatch_group_wait來實現多個操作的join行為,代碼如下: 複製代碼 1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 2 dispatch_group_t group = dispatch_group_create(); 3 4 dispatch_group_async(group, queue, ^{ 5 sleep(0.5); 6 NSLog(@"1"); 7 }); 8 dispatch_group_async(group, queue, ^{ 9 sleep(1.5);10 NSLog(@"2");11 });12 dispatch_group_async(group, queue, ^{13 sleep(2.5);14 NSLog(@"3");15 });16 17 NSLog(@"aaaaa");18 19 dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 2ull * NSEC_PER_SEC);20 if (dispatch_group_wait(group, time) == 0) {21 NSLog(@"已經全部執行完畢");22 }23 else {24 NSLog(@"沒有執行完畢");25 }26 27 NSLog(@"bbbbb");複製代碼這裡起了3個非同步線程放在一個組裡,之後通過dispatch_time_t建立了一個逾時時間(2秒),程式之後行,立即輸出了aaaaa,這是主線程輸出的,當遇到dispatch_group_wait時,主線程會被掛起,等待2秒,在等待的過程當中,子線程分別輸出了1和2,2秒時間達到後,主線程發現組裡的任務並沒有全部結束,然後輸出了bbbbb。 在這裡,如果逾時時間設定得比較長(比如5秒),那麼會在2.5秒時第三個任務結束後,立即輸出bbbbb,也就是說,當組中的任務全部執行完畢時,主線程就不再被阻塞了。 如果希望永久等待下去,時間可以設定為DISPATCH_TIME_FOREVER。 並行迴圈 類似於C#的PLINQ,OC也可以讓迴圈並存執行,在GCD當中有一個dispatch_apply函數: 1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);2 dispatch_apply(20, queue, ^(size_t i) {3 NSLog(@"%lu", i);4 });這段代碼讓i並行迴圈了20次,如果內部處理的是一個數組,就可以實現對數組的並行迴圈了,它的內部是dispatch_sync的同步操作,所以在執行這個迴圈的過程當中,當前線程會被阻塞。