今天我们来讨论一个经常出现的需求场景,也是一个老话题。在开发中我们往往会遇到需要进行多个网络请求,并且需要多个网络请求成功返回后再做其他事的场景。比如同一个界面显示的内容需要用到两个网络接口,而需求又希望成功返回两个接口的数据再进行页面展示;又比如喜欢挖坑的后台同学就只提供了返回一条数据的接口,但需求却希望我们在一个界面同时显示几条数据的情况。
我们不讨论什么执行完一个请求再执行一个这种串行的低效率方法,以下分析都是在异步的基础上进行的。
废话少说,直奔正题!先上个网络请求的模拟代码。
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 }
对于这样的需求,我们自然而然就想到了使用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已创建的线程。
这里我们使用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的线程必须等待进行具体网络请求的线程执行结束后再能继续执行,否则继续等待,相对上一种方案来说,这个线程就是刚好多出来的线程,这在上一种方案是不存在的。也就是刚好多了一倍。虽然有代价,但是我们的任务也算顺利完成了。
使用信号量
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
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。