在App開發中經常會遇到多個線程同時向服務器取數據, 如果每個線程取得數據后都去刷新UI會造成界面的閃爍
也有可能出現部分數據還沒有獲取完畢造成程序crash
之前在網上看到很多是利用dispatch_group_async
、dispatch_group_t
與dispatch_group_notify
組合來實現的
比如這樣:
將幾個線程加入到group中, 然后利用group_notify來執行最后要做的動作
- (void)viewDidLoad { [super viewDidLoad]; //創建一個group dispatch_group_t group = dispatch_group_create(); //創建一個隊列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //創建一個GCD線程1 dispatch_group_async(group, queue, ^{ NSLog(@"線程1"); }); //創建一個GCD線程2 dispatch_group_async(group, queue, ^{ NSLog(@"線程2"); }); //創建一個GCD線程3 dispatch_group_async(group, queue, ^{ NSLog(@"線程3"); }); //創建一個group通知 dispatch_group_notify(group, queue, ^{ NSLog(@"結束"); }); }
運行結果:
2017-01-18 11:49:22.454 GCDDemo[1375:107838] 線程2 2017-01-18 11:49:22.454 GCDDemo[1375:107837] 線程3 2017-01-18 11:49:22.454 GCDDemo[1375:107840] 線程1 2017-01-18 11:49:22.454 GCDDemo[1375:107840] 結束
看起來是3個線程無序運行, 最后等全部線程結束后才執行group結束動作. 看樣子都很正常
但如果3個線程為異步操作呢, 比如網絡請求
我們用異步計數試試看
- (void)viewDidLoad { [super viewDidLoad]; //創建一個group dispatch_group_t group = dispatch_group_create(); //創建一個隊列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //創建一個GCD線程1 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程1"); }); }); //創建一個GCD線程2 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程2"); }); }); //創建一個GCD線程3 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程3"); }); }); //創建一個group通知 dispatch_group_notify(group, queue, ^{ NSLog(@"結束"); }); }
運行結果為:
2017-01-18 13:31:57.610 GCDDemo[1528:138162] 結束 2017-01-18 13:31:57.621 GCDDemo[1528:138079] 線程1 2017-01-18 13:31:57.621 GCDDemo[1528:138079] 線程2 2017-01-18 13:31:57.622 GCDDemo[1528:138079] 線程3
看, 這樣就出問題了 先運行了我們原本要等線程都完成后才執行的動作
那要如何解決這個問題呢?
正確的方法應該是以上三個函數再配合
dispatch_group_enter(group)
和dispatch_group_leave(group)
兩個函數一起來使用,這樣才能實現我們想要的最終效果。
dispatch_group_enter(dispatch_group_t group)
參數group不能為空,在異步任務開始前調用。
它明確的表明了一個 block 被加入到了隊列組group中,此時group中的任務的引用計數會加1(類似於OC的內存管理)
,
dispatch_group_enter(group)
必須與dispatch_group_leave(group)
配對使用,
它們可以在使用dispatch_group_async
時幫助你合理的管理隊列組中任務的引用計數的增加與減少。
dispatch_group_leave(dispatch_group_t group)
參數group不能為空,在異步任務成功返回后調用。
它明確的表明了隊列組里的一個 block 已經執行完成,隊列組中的任務的引用計數會減1,
它必須與dispatch_group_enter(group)
配對使用,dispatch_group_leave(group)
的調用次數不能多於dispatch_group_enter(group)
的調用次數。
當隊列組里的任務的引用計數等於0時,會調用dispatch_group_notify
函數。
我們試試看, 注意紅色字體代碼
- (void)viewDidLoad { [super viewDidLoad]; //創建一個group __block dispatch_group_t group = dispatch_group_create(); //創建一個隊列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //group任務計數加3 dispatch_group_enter(group); dispatch_group_enter(group); dispatch_group_enter(group); //創建一個GCD線程1 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程1"); //group任務計數減1 dispatch_group_leave(group); }); }); //創建一個GCD線程2 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程2"); //group任務計數減1 dispatch_group_leave(group); }); }); //創建一個GCD線程3 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程3"); //group任務計數減1 dispatch_group_leave(group); }); }); //創建一個group通知, 任務計數為0時自動調用 dispatch_group_notify(group, queue, ^{ NSLog(@"結束"); }); }
運行結果:
2017-01-18 13:46:59.988 GCDDemo[1564:144979] 線程1 2017-01-18 13:46:59.991 GCDDemo[1564:144979] 線程2 2017-01-18 13:46:59.991 GCDDemo[1564:144979] 線程3 2017-01-18 13:46:59.993 GCDDemo[1564:145035] 結束
這樣就符合我們的預期了
還沒結束, 不 上面的方法是可以正確的實現多線程同步了, 現在我們再看下另外一種解決辦法
利用GCD信號量dispatch_semaphore_t來實現,
我們先看下什么是信號量
首先了解下信號量的幾個方法
1.dispatch_semaphore_create(long value); 創建信號量,傳入的value值要大於等於0,返回一個信號量 2.dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); 如果信號量的value值大於0,則會往下執行並將value的值減1,否則,阻礙當前線程並等待timeout后再往下執行。如果等待的期間desema的值被dispatch_semaphore_signal函數加1了,且該函數所處線程獲得了信號量,那么就繼續向下執行並將信號量減1。如果等待期間沒有獲取到信號量或者信號量的值一直為0,那么等到timeout時,其所處線程會自動往下執行。 3.dispatch_semaphore_signal(dispatch_semaphore_t dsema); 返回值為long類型,當返回值為0時表示當前並沒有線程等待其處理的信號量,其處理的信號量的值加1即可。當返回值不為0時,表示其當前有(一個或多個)線程等待其處理的信號量,並且該函數喚醒了一個等待的線程(當線程有優先級時,喚醒優先級最高的線程;否則隨機喚醒)。
實現過程:
- 創建一個任務組dispatch_group
dispatch_group_t group = dispatch_group_create();
- 將每個請求包裝成一個任務異步提交到任務組里,每個任務在一開始創建一個信號量,value值為0,任務最后在網絡請求完成前進行信號量的等待,如果網絡請求完成,則調用 'dispatch_semaphore_signal(semaphore);'對信號值加1,則線程不再進行信號量的等待,繼續往下執行。
- 當所有請求都完成時,會在dispatch_group_notify里的回調進行相應的處理。
我們上代碼看看:
- (void)viewDidLoad { [super viewDidLoad]; //創建一個group __block dispatch_group_t group = dispatch_group_create(); //創建一個隊列 __block dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //創建一個信號量 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); //創建一個GCD線程1 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程1"); //完成迭代后, 增加信號量 dispatch_semaphore_signal(semaphore); }); //在迭代完成之前, 信號量等待 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); }); //創建一個GCD線程2 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程2"); //完成迭代后, 增加信號量 dispatch_semaphore_signal(semaphore); }); //在迭代完成之前, 信號量等待 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); }); //創建一個GCD線程3 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程3"); //完成迭代后, 增加信號量 dispatch_semaphore_signal(semaphore); }); //在迭代完成之前, 信號量等待 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); }); //創建一個group通知, 任務計數為0時自動調用 dispatch_group_notify(group, queue, ^{ NSLog(@"結束"); }); }
這樣也實現了同步實現異步線程, 可能大家會有一個疑問, 不同線程之前的信號量是否會相互干擾呢,
或者說如果其中一個線程要耗費相當大的時間, 其他線程是否也會被阻塞呢,
我們來試驗下, 給線程3多增加幾個迭代, 然后在wait前后加上一下打印
- (void)viewDidLoad { [super viewDidLoad]; //創建一個group __block dispatch_group_t group = dispatch_group_create(); //創建一個隊列 __block dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //創建一個信號量 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); //創建一個GCD線程1 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程1"); //完成迭代后, 增加信號量 dispatch_semaphore_signal(semaphore); }); //在迭代完成之前, 信號量等待 NSLog(@"線程1等待"); dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"線程1完成"); }); //創建一個GCD線程2 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程2"); //完成迭代后, 增加信號量 dispatch_semaphore_signal(semaphore); }); //在迭代完成之前, 信號量等待 NSLog(@"線程2等待"); dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"線程2完成"); }); //創建一個GCD線程3 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 100000; i ++) { for (int i = 0; i < 100000; i ++) { } } NSLog(@"線程3"); //完成迭代后, 增加信號量 dispatch_semaphore_signal(semaphore); }); //在迭代完成之前, 信號量等待 NSLog(@"線程3等待"); dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"線程3完成"); }); //創建一個group通知, 任務計數為0時自動調用 dispatch_group_notify(group, queue, ^{ NSLog(@"結束"); }); }
運行結果為:
2017-01-18 17:14:58.814 GCDDemo[1136:77412] 線程2等待 2017-01-18 17:14:58.814 GCDDemo[1136:77410] 線程1等待 2017-01-18 17:14:58.814 GCDDemo[1136:77409] 線程3等待 2017-01-18 17:14:58.823 GCDDemo[1136:77339] 線程1 2017-01-18 17:14:58.823 GCDDemo[1136:77339] 線程2 2017-01-18 17:14:58.823 GCDDemo[1136:77412] 線程2完成 2017-01-18 17:14:58.823 GCDDemo[1136:77410] 線程1完成 2017-01-18 17:15:17.793 GCDDemo[1136:77339] 線程3 2017-01-18 17:15:17.793 GCDDemo[1136:77409] 線程3完成 2017-01-18 17:15:17.794 GCDDemo[1136:77409] 結束
好像看起來線程1, 2沒有受到線程3的影響
我們再增加線程3的耗時看看,
//創建一個GCD線程3 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 1000000; i ++) { for (int i = 0; i < 1000000; i ++) { } } NSLog(@"線程3"); //完成迭代后, 增加信號量 dispatch_semaphore_signal(semaphore); }); //在迭代完成之前, 信號量等待 NSLog(@"線程3等待"); dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"線程3完成"); });
我們多運行幾次, 發現會出現這樣的結果
2017-01-18 17:24:37.975 GCDDemo[1207:83821] 線程3等待 2017-01-18 17:24:37.975 GCDDemo[1207:83829] 線程2等待 2017-01-18 17:24:37.975 GCDDemo[1207:83818] 線程1等待 2017-01-18 17:24:37.984 GCDDemo[1207:83786] 線程1 2017-01-18 17:24:37.984 GCDDemo[1207:83786] 線程2 2017-01-18 17:24:37.984 GCDDemo[1207:83821] 線程3完成 2017-01-18 17:24:37.985 GCDDemo[1207:83829] 線程2完成
線程3先打印了執行完, 所以看不同線程去偵測同一個信號量的時候是會有干擾的, 但是還是會等全部線程執行結束后才會去執行notify動作
那給每一個線程分別創建一個信號量呢?
- (void)viewDidLoad { [super viewDidLoad]; //創建一個group __block dispatch_group_t group = dispatch_group_create(); //創建一個隊列 __block dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //創建一個信號量 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); dispatch_semaphore_t semaphore2 = dispatch_semaphore_create(0); dispatch_semaphore_t semaphore3 = dispatch_semaphore_create(0); //創建一個GCD線程1 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程1"); //完成迭代后, 增加信號量 dispatch_semaphore_signal(semaphore); }); //在迭代完成之前, 信號量等待 NSLog(@"線程1等待"); dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"線程1完成"); }); //創建一個GCD線程2 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 10000; i ++) { } NSLog(@"線程2"); //完成迭代后, 增加信號量 dispatch_semaphore_signal(semaphore2); }); //在迭代完成之前, 信號量等待 NSLog(@"線程2等待"); dispatch_semaphore_wait(semaphore2, DISPATCH_TIME_FOREVER); NSLog(@"線程2完成"); }); //創建一個GCD線程3 dispatch_group_async(group, queue, ^{ //模擬異步耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i < 1000000; i ++) { for (int i = 0; i < 1000000; i ++) { } } NSLog(@"線程3"); //完成迭代后, 增加信號量 dispatch_semaphore_signal(semaphore3); }); //在迭代完成之前, 信號量等待 NSLog(@"線程3等待"); dispatch_semaphore_wait(semaphore3, DISPATCH_TIME_FOREVER); NSLog(@"線程3完成"); }); //創建一個group通知, 任務計數為0時自動調用 dispatch_group_notify(group, queue, ^{ NSLog(@"結束"); }); }
多運行幾次, 看起來每次都是只有線程3等待, 1, 2線程會自己正常完成
這樣就OK了, 所以盡量每一個線程創建一個信號量, 避免相互干擾