前言 今天我們來討論一個經常出現的需求場景,也是一個老話題。在開發中我們往往會遇到需要進行多個網路請求,並且需要多個網路請求成功返回後再做其他事的場景。比如同一個界面顯示的內容需要用到兩個網路介面,而需求又希望成功返回兩個介面的數據再進行頁面展示;又比如喜歡挖坑的後臺同學就只提供了返回一條數據的介面 ...
前言
今天我們來討論一個經常出現的需求場景,也是一個老話題。在開發中我們往往會遇到需要進行多個網路請求,並且需要多個網路請求成功返回後再做其他事的場景。比如同一個界面顯示的內容需要用到兩個網路介面,而需求又希望成功返回兩個介面的數據再進行頁面展示;又比如喜歡挖坑的後臺同學就只提供了返回一條數據的介面,但需求卻希望我們在一個界面同時顯示幾條數據的情況。
正題
我們不討論什麼執行完一個請求再執行一個這種串列的低效率方法,以下分析都是在非同步的基礎上進行的。
廢話少說,直奔正題!先上個網路請求的模擬代碼。
1 //模擬一個網路請求方法 get/post/put...etc 2 - (void)httpRequest:(NSString *)method param:(NSDictionary *)param completion:(void(^)(id response))block{ 3 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 4 NSString *commend = [param objectForKey:commandKey]; 5 NSLog(@"request:%@ run in thread:%@", commend, [NSThread currentThread]); 6 NSTimeInterval sleepInterval = arc4random() % 10; 7 [NSThread sleepForTimeInterval:sleepInterval]; 8 dispatch_async(dispatch_get_main_queue(), ^{ 9 NSLog(@"requset:%@ done!", commend); 10 block(nil); 11 }); 12 }); 13 }
不可行的直接使用group的方案
對於這樣的需求,我們自然而然就想到了使用GCD group,先上代碼
1 - (void)testUsingGroup1{ 2 NSArray *commandArray = @[@"requestcommand1", @"requestcommand2", @"requestcommand3", @"requestcommand4", @"requestcommand5"]; 3 dispatch_group_t group = dispatch_group_create(); 4 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 5 6 [commandArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 7 dispatch_group_async(group, queue, ^{ 8 NSLog(@"%@ in group thread:%@", obj, [NSThread currentThread]); 9 [self httpRequest:nil param:@{commandKey : obj} completion:^(id response) { 10 11 }]; 12 }); 13 }]; 14 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ 15 NSLog(@"all http request done!"); 16 NSLog(@"UI update in main thread!"); 17 }); 18 }
代碼很快寫完了,但卻存在問題,我們來看一下運行結果:
1 2017-04-29 21:49:27.336 TestMutiRequest[1345:82575] requestcommand2 in group thread:<NSThread: 0x600000262a40>{number = 4, name = (null)} 2 2017-04-29 21:49:27.336 TestMutiRequest[1345:82576] requestcommand1 in group thread:<NSThread: 0x60000007f2c0>{number = 3, name = (null)} 3 2017-04-29 21:49:27.336 TestMutiRequest[1345:82578] requestcommand3 in group thread:<NSThread: 0x608000263c00>{number = 5, name = (null)} 4 2017-04-29 21:49:27.336 TestMutiRequest[1345:82638] requestcommand4 in group thread:<NSThread: 0x600000262b00>{number = 6, name = (null)} 5 2017-04-29 21:49:27.336 TestMutiRequest[1345:82575] requestcommand5 in group thread:<NSThread: 0x600000262a40>{number = 4, name = (null)} 6 2017-04-29 21:49:27.336 TestMutiRequest[1345:82576] request:requestcommand2 run in thread:<NSThread: 0x60000007f2c0>{number = 3, name = (null)} 7 2017-04-29 21:49:27.337 TestMutiRequest[1345:82639] request:requestcommand1 run in thread:<NSThread: 0x608000264000>{number = 7, name = (null)} 8 2017-04-29 21:49:27.337 TestMutiRequest[1345:82578] request:requestcommand3 run in thread:<NSThread: 0x608000263c00>{number = 5, name = (null)} 9 2017-04-29 21:49:27.337 TestMutiRequest[1345:82638] request:requestcommand4 run in thread:<NSThread: 0x600000262b00>{number = 6, name = (null)} 10 2017-04-29 21:49:27.338 TestMutiRequest[1345:82575] request:requestcommand5 run in thread:<NSThread: 0x600000262a40>{number = 4, name = (null)} 11 2017-04-29 21:49:27.391 TestMutiRequest[1345:82507] all http request done! 12 2017-04-29 21:49:27.429 TestMutiRequest[1345:82507] UI update in main thread! 13 2017-04-29 21:49:27.432 TestMutiRequest[1345:82507] requset:requestcommand2 done! 14 2017-04-29 21:49:27.435 TestMutiRequest[1345:82507] requset:requestcommand3 done! 15 2017-04-29 21:49:27.437 TestMutiRequest[1345:82507] requset:requestcommand4 done! 16 2017-04-29 21:49:28.347 TestMutiRequest[1345:82507] requset:requestcommand5 done! 17 2017-04-29 21:49:35.399 TestMutiRequest[1345:82507] requset:requestcommand1 done!
註意這裡:
1 2017-04-29 21:49:27.338 TestMutiRequest[1345:82575] request:requestcommand5 run in thread:<NSThread: 0x600000262a40>{number = 4, name = (null)} 2 2017-04-29 21:49:27.391 TestMutiRequest[1345:82507] all http request done! 3 2017-04-29 21:49:27.429 TestMutiRequest[1345:82507] UI update in main thread! 4 2017-04-29 21:49:27.432 TestMutiRequest[1345:82507] requset:requestcommand2 done!
結果很明顯,並不能得出我們需要的結果!!
問題究竟出現哪呢?!!
讓我們在回顧一下group的概念,group的設計就是為了方便我們執行完一系列的任務之後再執行其他的任務,但是不能忽視的是,這裡的任務是有要求的,這裡的任務必須要是同步執行的!!如果任務是非同步的,group只會執行完任務裡面非同步之前的代碼以及分發非同步任務就返回了!!也就代表分發group的當前這個任務完成了!但事實卻是這個任務的一部分子任務在其他線程執行了,而且不一定已執行結束返回。
問題分析到這裡,理所當然的就會出現以上結果的問題。
解決的方案也很自然想法了,就是想辦法使分發到group的任務是同步執行的。
順便提一點,雖然任務未能順利完成,但我們可以註意到GCD實現的一些細節,在這裡group到另外非同步方法的執行,GCD並沒有重新創建新的線程,而是重用了group已創建的線程。
改進的group方案
這裡我們使用dispatch_semaphore_t使單個請求任務同步執行。
1 - (void)httpRequest2:(NSString *)method param:(NSDictionary *)param completion:(void(^)(id response))block{ 2 dispatch_semaphore_t sem = dispatch_semaphore_create(0); 3 [self httpRequest:method param:param completion:^(id response) { 4 if (block) { 5 block(response); 6 } 7 dispatch_semaphore_signal(sem); 8 }]; 9 dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 10 }
testUsingGroup方法也相應改成對httpRequest2方法的調用
1 - (void)testUsingGroup2{ 2 NSArray *commandArray = @[@"requestcommand1", @"requestcommand2", @"requestcommand3", @"requestcommand4", @"requestcommand5"]; 3 dispatch_group_t group = dispatch_group_create(); 4 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 5 6 [commandArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 7 dispatch_group_async(group, queue, ^{ 8 NSLog(@"%@ in group thread:%@", obj, [NSThread currentThread]); 9 [self httpRequest2:nil param:@{commandKey : obj} completion:^(id response) { 10 11 }]; 12 }); 13 }]; 14 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ 15 NSLog(@"all http request done!"); 16 NSLog(@"UI update in main thread!"); 17 }); 18 }
我們再來看一下結果:
1 2017-04-29 22:02:01.744 TestMutiRequest[1381:90462] requestcommand4 in group thread:<NSThread: 0x60000007bfc0>{number = 6, name = (null)} 2 2017-04-29 22:02:01.744 TestMutiRequest[1381:90404] requestcommand2 in group thread:<NSThread: 0x608000073880>{number = 4, name = (null)} 3 2017-04-29 22:02:01.744 TestMutiRequest[1381:90406] requestcommand1 in group thread:<NSThread: 0x60000007ba80>{number = 3, name = (null)} 4 2017-04-29 22:02:01.744 TestMutiRequest[1381:90403] requestcommand3 in group thread:<NSThread: 0x60000007bec0>{number = 5, name = (null)} 5 2017-04-29 22:02:01.745 TestMutiRequest[1381:90463] requestcommand5 in group thread:<NSThread: 0x60000007be80>{number = 7, name = (null)} 6 2017-04-29 22:02:01.745 TestMutiRequest[1381:90464] request:requestcommand4 run in thread:<NSThread: 0x60000007c440>{number = 8, name = (null)} 7 2017-04-29 22:02:01.746 TestMutiRequest[1381:90465] request:requestcommand2 run in thread:<NSThread: 0x60000007c4c0>{number = 9, name = (null)} 8 2017-04-29 22:02:01.746 TestMutiRequest[1381:90466] request:requestcommand1 run in thread:<NSThread: 0x60000007c540>{number = 10, name = (null)} 9 2017-04-29 22:02:01.746 TestMutiRequest[1381:90464] request:requestcommand3 run in thread:<NSThread: 0x60000007c440>{number = 8, name = (null)} 10 2017-04-29 22:02:01.746 TestMutiRequest[1381:90467] request:requestcommand5 run in thread:<NSThread: 0x608000073ec0>{number = 11, name = (null)} 11 2017-04-29 22:02:01.751 TestMutiRequest[1381:90356] requset:requestcommand4 done! 12 2017-04-29 22:02:01.821 TestMutiRequest[1381:90356] requset:requestcommand3 done! 13 2017-04-29 22:02:02.817 TestMutiRequest[1381:90356] requset:requestcommand1 done! 14 2017-04-29 22:02:03.796 TestMutiRequest[1381:90356] requset:requestcommand5 done! 15 2017-04-29 22:02:07.817 TestMutiRequest[1381:90356] requset:requestcommand2 done! 16 2017-04-29 22:02:07.818 TestMutiRequest[1381:90356] all http request done! 17 2017-04-29 22:02:07.818 TestMutiRequest[1381:90356] UI update in main thread!
這個結果就是我們預期希望得到的!
但是不能高興的太早,這個方法需要實現了我們的需求,但是確實存在問題的。我們可以看一下當前的線程情況。整整比上一種不可行方案多出了一倍的線程數(5條線程->10條線程)!!
這是怎麼發生的呢?剛好是因為我們把請求方法改成了同步的,但是網路請求是在其他線程執行的!系統分配給執行httpRequest2的線程必須等待進行具體網路請求的線程執行結束後再能繼續執行,否則繼續等待,相對上一種方案來說,這個線程就是剛好多出來的線程,這在上一種方案是不存在的。也就是剛好多了一倍。雖然有代價,但是我們的任務也算順利完成了。
不額外增加多一倍線程的方法(dispatch_semaphore_t)
使用信號量
1 - (void)testUsingSemaphore{ 2 dispatch_semaphore_t sem = dispatch_semaphore_create(0); 3 4 NSArray *commandArray = @[@"requestcommand1", @"requestcommand2", @"requestcommand3", @"requestcommand4", @"requestcommand5"]; 5 6 NSInteger commandCount = [commandArray count]; 7 //代表http訪問返回的數量 8 //這裡模仿的http請求block塊都是在同一線程(主線程)執行返回的,所以對這個變數的訪問不存在資源競爭問題,故不需要枷鎖處理 9 //如果網路請求在不同線程返回,要對這個變數進行枷鎖處理,不然很會有資源競爭危險 10 __block NSInteger httpFinishCount = 0; 11 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 12 //demo testUsingSemaphore方法是在主線程調用的,不直接調用遍歷執行,而是嵌套了一個非同步,是為了避免主線程阻塞 13 NSLog(@"start all http dispatch in thread: %@", [NSThread currentThread]); 14 [commandArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 15 [self httpRequest:nil param:@{commandKey : obj} completion:^(id response) { 16 //全部請求返回才觸發signal 17 if (++httpFinishCount == commandCount) { 18 dispatch_semaphore_signal(sem); 19 } 20 }]; 21 }]; 22 //如果全部請求沒有返回則該線程會一直阻塞 23 dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 24 NSLog(@"all http request done! end thread: %@", [NSThread currentThread]); 25 dispatch_async(dispatch_get_main_queue(), ^{ 26 NSLog(@"UI update in main thread!"); 27 }); 28 }); 29 30 }
這是種可行的方法,思路很簡單:任務分發線程進行等待,所有網路請求成功返回後才發送信號量,任務分發線程繼續執行。直接上結果:
1 2017-04-29 22:25:45.498 TestMutiRequest[1469:105980] start all http dispatch in thread: <NSThread: 0x608000260680>{number = 3, name = (null)} 2 2017-04-29 22:25:45.499 TestMutiRequest[1469:106008] request:requestcommand3 run in thread:<NSThread: 0x60000007ec80>{number = 6, name = (null)} 3 2017-04-29 22:25:45.499 TestMutiRequest[1469:105983] request:requestcommand2 run in thread:<NSThread: 0x608000260c00>{number = 5, name = (null)} 4 2017-04-29 22:25:45.499 TestMutiRequest[1469:105981] request:requestcommand1 run in thread:<NSThread: 0x60000007e000>{number = 4, name = (null)} 5 2017-04-29 22:25:45.499 TestMutiRequest[1469:106009] request:requestcommand4 run in thread:<NSThread: 0x608000260d40>{number = 7, name = (null)} 6 2017-04-29 22:25:45.499 TestMutiRequest[1469:106010] request:requestcommand5 run in thread:<NSThread: 0x608000260b80>{number = 8, name = (null)} 7 2017-04-29 22:25:45.519 TestMutiRequest[1469:105944] requset:requestcommand1 done! 8 2017-04-29 22:25:47.500 TestMutiRequest[1469:105944] requset:requestcommand4 done! 9 2017-04-29 22:25:49.559 TestMutiRequest[1469:105944] requset:requestcommand3 done! 10 2017-04-29 22:25:50.558 TestMutiRequest[1469:105944] requset:requestcommand5 done! 11 2017-04-29 22:25:52.571 TestMutiRequest[1469:105944] requset:requestcommand2 done! 12 2017-04-29 22:25:52.572 TestMutiRequest[1469:105980] all http request done! end thread: <NSThread: 0x608000260680>{number = 3, name = (null)} 13 2017-04-29 22:25:52.572 TestMutiRequest[1469:105944] UI update in main thread!
結論:
從效率和資源使用的角度來看,最後一種只使用信號量的方法是最好的,但是卻有點使用一個外部變數來保存執行次數的嫌疑。
另外本文的思路不僅適用於網路請求這種場景,其他需要非同步執行的類似場景也是適用的,比如資料庫操作等。
個人水平有限,如果你有更好的方案或者覺得不對的地方,隨時歡迎在評論留言交流學習!!
最後附上demo地址:
https://github.com/Calvix-Xu/TestMultiRequest.git
作者:Calvix
鏈接:http://www.jianshu.com/p/46f1314ed947
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。