前言
今天我們來(lái)討論一個(gè)經(jīng)常出現(xiàn)的需求場(chǎng)景酪捡,也是一個(gè)老話題。在開(kāi)發(fā)中我們往往會(huì)遇到需要進(jìn)行多個(gè)網(wǎng)絡(luò)請(qǐng)求,并且需要多個(gè)網(wǎng)絡(luò)請(qǐng)求成功返回后再做其他事的場(chǎng)景陈症。比如同一個(gè)界面顯示的內(nèi)容需要用到兩個(gè)網(wǎng)絡(luò)接口,而需求又希望成功返回兩個(gè)接口的數(shù)據(jù)再進(jìn)行頁(yè)面展示震糖;又比如喜歡挖坑的后臺(tái)同學(xué)就只提供了返回一條數(shù)據(jù)的接口录肯,但需求卻希望我們?cè)谝粋€(gè)界面同時(shí)顯示幾條數(shù)據(jù)的情況。
正題
我們不討論什么執(zhí)行完一個(gè)請(qǐng)求再執(zhí)行一個(gè)這種串行的低效率方法吊说,以下分析都是在異步的基礎(chǔ)上進(jìn)行的论咏。
廢話少說(shuō)优炬,直奔正題!先上個(gè)網(wǎng)絡(luò)請(qǐng)求的模擬代碼厅贪。
//模擬一個(gè)網(wǎng)絡(luò)請(qǐng)求方法 get/post/put...etc
- (void)httpRequest:(NSString *)method param:(NSDictionary *)param completion:(void(^)(id response))block{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *commend = [param objectForKey:commandKey];
NSLog(@"request:%@ run in thread:%@", commend, [NSThread currentThread]);
NSTimeInterval sleepInterval = arc4random() % 10;
[NSThread sleepForTimeInterval:sleepInterval];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"requset:%@ done!", commend);
block(nil);
});
});
}
不可行的直接使用group的方案
對(duì)于這樣的需求蠢护,我們自然而然就想到了使用GCD group,先上代碼
- (void)testUsingGroup1{
NSArray *commandArray = @[@"requestcommand1", @"requestcommand2", @"requestcommand3", @"requestcommand4", @"requestcommand5"];
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
[commandArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
dispatch_group_async(group, queue, ^{
NSLog(@"%@ in group thread:%@", obj, [NSThread currentThread]);
[self httpRequest:nil param:@{commandKey : obj} completion:^(id response) {
}];
});
}];
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"all http request done!");
NSLog(@"UI update in main thread!");
});
}
代碼很快寫(xiě)完了养涮,但卻存在問(wèn)題葵硕,我們來(lái)看一下運(yùn)行結(jié)果:
2017-04-29 21:49:27.336 TestMutiRequest[1345:82575] requestcommand2 in group thread:<NSThread: 0x600000262a40>{number = 4, name = (null)}
2017-04-29 21:49:27.336 TestMutiRequest[1345:82576] requestcommand1 in group thread:<NSThread: 0x60000007f2c0>{number = 3, name = (null)}
2017-04-29 21:49:27.336 TestMutiRequest[1345:82578] requestcommand3 in group thread:<NSThread: 0x608000263c00>{number = 5, name = (null)}
2017-04-29 21:49:27.336 TestMutiRequest[1345:82638] requestcommand4 in group thread:<NSThread: 0x600000262b00>{number = 6, name = (null)}
2017-04-29 21:49:27.336 TestMutiRequest[1345:82575] requestcommand5 in group thread:<NSThread: 0x600000262a40>{number = 4, name = (null)}
2017-04-29 21:49:27.336 TestMutiRequest[1345:82576] request:requestcommand2 run in thread:<NSThread: 0x60000007f2c0>{number = 3, name = (null)}
2017-04-29 21:49:27.337 TestMutiRequest[1345:82639] request:requestcommand1 run in thread:<NSThread: 0x608000264000>{number = 7, name = (null)}
2017-04-29 21:49:27.337 TestMutiRequest[1345:82578] request:requestcommand3 run in thread:<NSThread: 0x608000263c00>{number = 5, name = (null)}
2017-04-29 21:49:27.337 TestMutiRequest[1345:82638] request:requestcommand4 run in thread:<NSThread: 0x600000262b00>{number = 6, name = (null)}
2017-04-29 21:49:27.338 TestMutiRequest[1345:82575] request:requestcommand5 run in thread:<NSThread: 0x600000262a40>{number = 4, name = (null)}
2017-04-29 21:49:27.391 TestMutiRequest[1345:82507] all http request done!
2017-04-29 21:49:27.429 TestMutiRequest[1345:82507] UI update in main thread!
2017-04-29 21:49:27.432 TestMutiRequest[1345:82507] requset:requestcommand2 done!
2017-04-29 21:49:27.435 TestMutiRequest[1345:82507] requset:requestcommand3 done!
2017-04-29 21:49:27.437 TestMutiRequest[1345:82507] requset:requestcommand4 done!
2017-04-29 21:49:28.347 TestMutiRequest[1345:82507] requset:requestcommand5 done!
2017-04-29 21:49:35.399 TestMutiRequest[1345:82507] requset:requestcommand1 done!
注意這里:
2017-04-29 21:49:27.338 TestMutiRequest[1345:82575] request:requestcommand5 run in thread:<NSThread: 0x600000262a40>{number = 4, name = (null)}
2017-04-29 21:49:27.391 TestMutiRequest[1345:82507] all http request done!
2017-04-29 21:49:27.429 TestMutiRequest[1345:82507] UI update in main thread!
2017-04-29 21:49:27.432 TestMutiRequest[1345:82507] requset:requestcommand2 done!
結(jié)果很明顯,并不能得出我們需要的結(jié)果9嵯拧贬芥!
問(wèn)題究竟出現(xiàn)哪呢?P觥蘸劈!
讓我們?cè)诨仡櫼幌耮roup的概念,group的設(shè)計(jì)就是為了方便我們執(zhí)行完一系列的任務(wù)之后再執(zhí)行其他的任務(wù)尊沸,但是不能忽視的是威沫,這里的任務(wù)是有要求的,這里的任務(wù)必須要是同步執(zhí)行的M葑ā棒掠!如果任務(wù)是異步的,group只會(huì)執(zhí)行完任務(wù)里面異步之前的代碼以及分發(fā)異步任務(wù)就返回了Fㄉ獭烟很!也就代表分發(fā)group的當(dāng)前這個(gè)任務(wù)完成了!但事實(shí)卻是這個(gè)任務(wù)的一部分子任務(wù)在其他線程執(zhí)行了蜡镶,而且不一定已執(zhí)行結(jié)束返回雾袱。
問(wèn)題分析到這里,理所當(dāng)然的就會(huì)出現(xiàn)以上結(jié)果的問(wèn)題官还。
解決的方案也很自然想法了芹橡,就是想辦法使分發(fā)到group的任務(wù)是同步執(zhí)行的。
順便提一點(diǎn)望伦,雖然任務(wù)未能順利完成林说,但我們可以注意到GCD實(shí)現(xiàn)的一些細(xì)節(jié),在這里group到另外異步方法的執(zhí)行屯伞,GCD并沒(méi)有重新創(chuàng)建新的線程腿箩,而是重用了group已創(chuàng)建的線程。
改進(jìn)的group方案
這里我們使用dispatch_semaphore_t使單個(gè)請(qǐng)求任務(wù)同步執(zhí)行劣摇。
- (void)httpRequest2:(NSString *)method param:(NSDictionary *)param completion:(void(^)(id response))block{
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
[self httpRequest:method param:param completion:^(id response) {
if (block) {
block(response);
}
dispatch_semaphore_signal(sem);
}];
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}
testUsingGroup方法也相應(yīng)改成對(duì)httpRequest2方法的調(diào)用
- (void)testUsingGroup2{
NSArray *commandArray = @[@"requestcommand1", @"requestcommand2", @"requestcommand3", @"requestcommand4", @"requestcommand5"];
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
[commandArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
dispatch_group_async(group, queue, ^{
NSLog(@"%@ in group thread:%@", obj, [NSThread currentThread]);
[self httpRequest2:nil param:@{commandKey : obj} completion:^(id response) {
}];
});
}];
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"all http request done!");
NSLog(@"UI update in main thread!");
});
}
我們?cè)賮?lái)看一下結(jié)果:
2017-04-29 22:02:01.744 TestMutiRequest[1381:90462] requestcommand4 in group thread:<NSThread: 0x60000007bfc0>{number = 6, name = (null)}
2017-04-29 22:02:01.744 TestMutiRequest[1381:90404] requestcommand2 in group thread:<NSThread: 0x608000073880>{number = 4, name = (null)}
2017-04-29 22:02:01.744 TestMutiRequest[1381:90406] requestcommand1 in group thread:<NSThread: 0x60000007ba80>{number = 3, name = (null)}
2017-04-29 22:02:01.744 TestMutiRequest[1381:90403] requestcommand3 in group thread:<NSThread: 0x60000007bec0>{number = 5, name = (null)}
2017-04-29 22:02:01.745 TestMutiRequest[1381:90463] requestcommand5 in group thread:<NSThread: 0x60000007be80>{number = 7, name = (null)}
2017-04-29 22:02:01.745 TestMutiRequest[1381:90464] request:requestcommand4 run in thread:<NSThread: 0x60000007c440>{number = 8, name = (null)}
2017-04-29 22:02:01.746 TestMutiRequest[1381:90465] request:requestcommand2 run in thread:<NSThread: 0x60000007c4c0>{number = 9, name = (null)}
2017-04-29 22:02:01.746 TestMutiRequest[1381:90466] request:requestcommand1 run in thread:<NSThread: 0x60000007c540>{number = 10, name = (null)}
2017-04-29 22:02:01.746 TestMutiRequest[1381:90464] request:requestcommand3 run in thread:<NSThread: 0x60000007c440>{number = 8, name = (null)}
2017-04-29 22:02:01.746 TestMutiRequest[1381:90467] request:requestcommand5 run in thread:<NSThread: 0x608000073ec0>{number = 11, name = (null)}
2017-04-29 22:02:01.751 TestMutiRequest[1381:90356] requset:requestcommand4 done!
2017-04-29 22:02:01.821 TestMutiRequest[1381:90356] requset:requestcommand3 done!
2017-04-29 22:02:02.817 TestMutiRequest[1381:90356] requset:requestcommand1 done!
2017-04-29 22:02:03.796 TestMutiRequest[1381:90356] requset:requestcommand5 done!
2017-04-29 22:02:07.817 TestMutiRequest[1381:90356] requset:requestcommand2 done!
2017-04-29 22:02:07.818 TestMutiRequest[1381:90356] all http request done!
2017-04-29 22:02:07.818 TestMutiRequest[1381:90356] UI update in main thread!
這個(gè)結(jié)果就是我們預(yù)期希望得到的珠移!
但是不能高興的太早,這個(gè)方法需要實(shí)現(xiàn)了我們的需求,但是確實(shí)存在問(wèn)題的剑梳。我們可以看一下當(dāng)前的線程情況唆貌。整整比上一種不可行方案多出了一倍的線程數(shù)(5條線程->10條線程);恕垢乙!
這是怎么發(fā)生的呢?剛好是因?yàn)槲覀儼颜?qǐng)求方法改成了同步的语卤,但是網(wǎng)絡(luò)請(qǐng)求是在其他線程執(zhí)行的追逮!系統(tǒng)分配給執(zhí)行httpRequest2的線程必須等待進(jìn)行具體網(wǎng)絡(luò)請(qǐng)求的線程執(zhí)行結(jié)束后再能繼續(xù)執(zhí)行,否則繼續(xù)等待粹舵,相對(duì)上一種方案來(lái)說(shuō)钮孵,這個(gè)線程就是剛好多出來(lái)的線程,這在上一種方案是不存在的眼滤。也就是剛好多了一倍巴席。雖然有代價(jià),但是我們的任務(wù)也算順利完成了诅需。
不額外增加多一倍線程的方法(dispatch_semaphore_t)
使用信號(hào)量
- (void)testUsingSemaphore{
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
NSArray *commandArray = @[@"requestcommand1", @"requestcommand2", @"requestcommand3", @"requestcommand4", @"requestcommand5"];
NSInteger commandCount = [commandArray count];
//代表http訪問(wèn)返回的數(shù)量
//這里模仿的http請(qǐng)求block塊都是在同一線程(主線程)執(zhí)行返回的漾唉,所以對(duì)這個(gè)變量的訪問(wèn)不存在資源競(jìng)爭(zhēng)問(wèn)題,故不需要枷鎖處理
//如果網(wǎng)絡(luò)請(qǐng)求在不同線程返回堰塌,要對(duì)這個(gè)變量進(jìn)行枷鎖處理赵刑,不然很會(huì)有資源競(jìng)爭(zhēng)危險(xiǎn)
__block NSInteger httpFinishCount = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//demo testUsingSemaphore方法是在主線程調(diào)用的,不直接調(diào)用遍歷執(zhí)行场刑,而是嵌套了一個(gè)異步般此,是為了避免主線程阻塞
NSLog(@"start all http dispatch in thread: %@", [NSThread currentThread]);
[commandArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[self httpRequest:nil param:@{commandKey : obj} completion:^(id response) {
//全部請(qǐng)求返回才觸發(fā)signal
if (++httpFinishCount == commandCount) {
dispatch_semaphore_signal(sem);
}
}];
}];
//如果全部請(qǐng)求沒(méi)有返回則該線程會(huì)一直阻塞
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
NSLog(@"all http request done! end thread: %@", [NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"UI update in main thread!");
});
});
}
這是種可行的方法,思路很簡(jiǎn)單:任務(wù)分發(fā)線程進(jìn)行等待牵现,所有網(wǎng)絡(luò)請(qǐng)求成功返回后才發(fā)送信號(hào)量铐懊,任務(wù)分發(fā)線程繼續(xù)執(zhí)行。直接上結(jié)果:
2017-04-29 22:25:45.498 TestMutiRequest[1469:105980] start all http dispatch in thread: <NSThread: 0x608000260680>{number = 3, name = (null)}
2017-04-29 22:25:45.499 TestMutiRequest[1469:106008] request:requestcommand3 run in thread:<NSThread: 0x60000007ec80>{number = 6, name = (null)}
2017-04-29 22:25:45.499 TestMutiRequest[1469:105983] request:requestcommand2 run in thread:<NSThread: 0x608000260c00>{number = 5, name = (null)}
2017-04-29 22:25:45.499 TestMutiRequest[1469:105981] request:requestcommand1 run in thread:<NSThread: 0x60000007e000>{number = 4, name = (null)}
2017-04-29 22:25:45.499 TestMutiRequest[1469:106009] request:requestcommand4 run in thread:<NSThread: 0x608000260d40>{number = 7, name = (null)}
2017-04-29 22:25:45.499 TestMutiRequest[1469:106010] request:requestcommand5 run in thread:<NSThread: 0x608000260b80>{number = 8, name = (null)}
2017-04-29 22:25:45.519 TestMutiRequest[1469:105944] requset:requestcommand1 done!
2017-04-29 22:25:47.500 TestMutiRequest[1469:105944] requset:requestcommand4 done!
2017-04-29 22:25:49.559 TestMutiRequest[1469:105944] requset:requestcommand3 done!
2017-04-29 22:25:50.558 TestMutiRequest[1469:105944] requset:requestcommand5 done!
2017-04-29 22:25:52.571 TestMutiRequest[1469:105944] requset:requestcommand2 done!
2017-04-29 22:25:52.572 TestMutiRequest[1469:105980] all http request done! end thread: <NSThread: 0x608000260680>{number = 3, name = (null)}
2017-04-29 22:25:52.572 TestMutiRequest[1469:105944] UI update in main thread!
結(jié)論:
從效率和資源使用的角度來(lái)看瞎疼,最后一種只使用信號(hào)量的方法是最好的居扒,但是卻有點(diǎn)使用一個(gè)外部變量來(lái)保存執(zhí)行次數(shù)的嫌疑。
另外本文的思路不僅適用于網(wǎng)絡(luò)請(qǐng)求這種場(chǎng)景丑慎,其他需要異步執(zhí)行的類(lèi)似場(chǎng)景也是適用的喜喂,比如數(shù)據(jù)庫(kù)操作等。
個(gè)人水平有限竿裂,如果你有更好的方案或者覺(jué)得不對(duì)的地方玉吁,隨時(shí)歡迎在評(píng)論留言交流學(xué)習(xí)!腻异!
最后附上demo地址:
https://github.com/Calvix-Xu/TestMultiRequest.git