一,GCD执行原理
1.GCD有一个底层线程池,这个池中存放的是一个个的线程。之所以称为“池”,很容易理解出这个“池”中的线程是可以重用的,当一段时间后这个线程没有被调用胡话,这个线程就会被销毁。注意:开多少条线程是由底层线程池决定的(线程建议控制再3~5条),池是系统自动来维护,不需要我们程序员来维护(看到这句话是不是很开心?) 而我们程序员需要关心的是什么呢?我们只关心的是向队列中添加任务,队列调度即可。
2.如果队列中存放的是同步任务,则任务出队后,底层线程池中会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。这样队列中的任务反复调度,因为是同步的,所以当我们用currentThread打印的时候,就是同一条线程。
3.如果队列中存放的是异步的任务,(注意异步可以开线程),当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后再回到底层线程池中。
4.就对线程完成一个复用,而不需要每一个任务执行都开启新的线程,也就从而节约的系统的开销,提高了效率。在iOS7.0的时候,使用GCD系统通常只能开58条线程,iOS8.0以后,系统可以开启很多条线程,但是实在开发应用中,建议开启线程条数:35条最为合理。
二,GCD Global队列创建线程进行耗时操作的风险
先思考下如下几个问题:
- 新建线程的方式有哪些?各自的优缺点是什么?
- dispatch_async 函数分发到全局队列一定会新建线程执行任务么?
- 如果全局队列对应的线程池如果满了,后续的派发的任务会怎么处置?有什么风险?
答案大致是这样的:dispatch_async 函数分发到全局队列不一定会新建线程执行任务,全局队列底层有一个的线程池,如果线程池满了,那么后续的任务会被 block 住,等待前面的任务执行完成,才会继续执行。如果线程池中的线程长时间不结束,后续堆积的任务会越来越多,此时就会存在 APP crash的风险。
比如:
- (void)dispatchTest1 {
for (NSInteger i = 0; i< 10000 ; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self dispatchTask:i];
});
}
}
- (void)dispatchTask:(NSInteger)index {
//模拟耗时操作,比如DB,网络,文件读写等等
sleep(30);
NSLog(@"----:%ld",index);
}
以上逻辑用真机测试会有卡死的几率,并非每次都会发生,但多尝试几次就会复现,伴随前后台切换,crash几率增大。
下面做一下分析:
参看 GCD 源码我们可以看到全局队列的相关源码如下:
DISPATCH_NOINLINE
static void
_dispatch_queue_wakeup_global_slow(dispatch_queue_t dq, unsigned int n)
{
dispatch_root_queue_context_t qc = dq->do_ctxt;
uint32_t i = n;
int r;
_dispatch_debug_root_queue(dq, __func__);
dispatch_once_f(&_dispatch_root_queues_pred, NULL,
_dispatch_root_queues_init);
#if HAVE_PTHREAD_WORKQUEUES
#if DISPATCH_USE_PTHREAD_POOL
if (qc->dgq_kworkqueue != (void*)(~0ul))
#endif
{
_dispatch_root_queue_debug("requesting new worker thread for global "
"queue: %p", dq);
#if DISPATCH_USE_LEGACY_WORKQUEUE_FALLBACK
if (qc->dgq_kworkqueue) {
pthread_workitem_handle_t wh;
unsigned int gen_cnt;
do {
r = pthread_workqueue_additem_np(qc->dgq_kworkqueue,
_dispatch_worker_thread4, dq, &wh, &gen_cnt);
(void)dispatch_assume_zero(r);
} while (--i);
return;
}
#endif // DISPATCH_USE_LEGACY_WORKQUEUE_FALLBACK
#if HAVE_PTHREAD_WORKQUEUE_SETDISPATCH_NP
if (!dq->dq_priority) {
r = pthread_workqueue_addthreads_np(qc->dgq_wq_priority,
qc->dgq_wq_options, (int)i);
(void)dispatch_assume_zero(r);
return;
}
#endif
#if HAVE_PTHREAD_WORKQUEUE_QOS
r = _pthread_workqueue_addthreads((int)i, dq->dq_priority);
(void)dispatch_assume_zero(r);
#endif
return;
}
#endif // HAVE_PTHREAD_WORKQUEUES
#if DISPATCH_USE_PTHREAD_POOL
dispatch_pthread_root_queue_context_t pqc = qc->dgq_ctxt;
if (fastpath(pqc->dpq_thread_mediator.do_vtable)) {
while (dispatch_semaphore_signal(&pqc->dpq_thread_mediator)) {
if (!--i) {
return;
}
}
}
uint32_t j, t_count;
// seq_cst with atomic store to tail
t_count = dispatch_atomic_load2o(qc, dgq_thread_pool_size, seq_cst);
do {
if (!t_count) {
_dispatch_root_queue_debug("pthread pool is full for root queue: "
"%p", dq);
return;
}
j = i > t_count ? t_count : i;
} while (!dispatch_atomic_cmpxchgvw2o(qc, dgq_thread_pool_size, t_count,
t_count - j, &t_count, acquire));
pthread_attr_t *attr = &pqc->dpq_thread_attr;
pthread_t tid, *pthr = &tid;
#if DISPATCH_ENABLE_PTHREAD_ROOT_QUEUES
if (slowpath(dq == &_dispatch_mgr_root_queue)) {
pthr = _dispatch_mgr_root_queue_init();
}
#endif
do {
_dispatch_retain(dq);
while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
if (r != EAGAIN) {
(void)dispatch_assume_zero(r);
}
_dispatch_temporary_resource_shortage();
}
} while (--j);
#endif // DISPATCH_USE_PTHREAD_POOL
}
对于执行的任务来说,所执行的线程具体是哪个线程,则是通过 GCD 的线程池(Thread Pool)来进行调度
上面贴的源码,我们关注如下的部分:
其中有一个用来记录线程池大小的字段 dgq_thread_pool_size
。这个字段标记着GCD
线程池的大小。摘录上面源码的一部分:
uint32_t j, t_count;
// seq_cst with atomic store to tail
t_count = dispatch_atomic_load2o(qc, dgq_thread_pool_size, seq_cst);
do {
if (!t_count) {
_dispatch_root_queue_debug("pthread pool is full for root queue: "
"%p", dq);
return;
}
j = i > t_count ? t_count : i;
} while (!dispatch_atomic_cmpxchgvw2o(qc, dgq_thread_pool_size, t_count,
t_count - j, &t_count, acquire));
也就是说:
全局队列的底层是一个线程池,向全局队列中提交的 block,都会被放到这个线程池中执行,如果线程池已满,后续再提交 block 就不会再重新创建线程。这就是为什么 Demo 会造成卡顿甚至冻屏的原因。
三,避免使用 GCD Global 队列创建 Runloop 常驻线程
在做网路请求时我们常常创建一个 Runloop 常驻线程用来接收、响应后续的服务端回执,比如NSURLConnection、AFNetworking等等,我们可以称这种线程为 Runloop 常驻线程。
正如上文所述,用 GCD Global 队列创建线程进行耗时操作是存在风险的。那么我们可以试想下,如果这个耗时操作变成了 runloop 常驻线程,会是什么结果?下面做一下分析:
先介绍下 Runloop 常驻线程的原理,在开发中一般有两种用法:
- 单一 Runloop 常驻线程:在 APP 的生命周期中开启了唯一的常驻线程来进行网络请求,常用于网络库,或者有维持长连接需求的库,比如: AFNetworking 、 SocketRocket。
- 多个 Runloop 常驻线程:每进行一次网络请求就开启一条 Runloop 常驻线程,这条线程的生命周期的起点是网络请求开始,终点是网络请求结束,或者网络请求超时。
单一 Runloop 常驻线程
先说第一种用法:
以 AFNetworking 为例,AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
多个 Runloop 常驻线程
第二种用法,我写了一个小 Demo 来模拟这种场景,
我们模拟了一个场景:假设所有的网络请求全部超时,或者服务端根本不响应,然后网络库超时检测机制的做法:
#import "Foo.h"
@interface Foo() {
NSRunLoop *_runloop;
NSTimer *_timeoutTimer;
NSTimeInterval _timeoutInterval;
dispatch_semaphore_t _sem;
}
@end
@implementation Foo
- (instancetype)init {
if (!(self = [super init])) {
return nil;
}
_timeoutInterval = 1 ;
_sem = dispatch_semaphore_create(0);
// Do any additional setup after loading the view, typically from a nib.
return self;
}
- (id)test {
// 第一种方式:
// NSThread *networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint0:) object:nil];
// [networkRequestThread start];
//第二种方式:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
[self networkRequestThreadEntryPoint0:nil];
});
dispatch_semaphore_wait(_sem, DISPATCH_TIME_FOREVER);
return @(YES);
}
- (void)networkRequestThreadEntryPoint0:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"CYLTest"];
_runloop = [NSRunLoop currentRunLoop];
[_runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
_timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(stopLoop) userInfo:nil repeats:NO];
[_runloop addTimer:_timeoutTimer forMode:NSRunLoopCommonModes];
[_runloop run];//在实际开发中最好使用这种方式来确保能runloop退出,做双重的保障[runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(timeoutInterval+5)]];
}
}
- (void)stopLoop {
CFRunLoopStop([_runloop getCFRunLoop]);
dispatch_semaphore_signal(_sem);
}
@end
如果
for (int i = 0; i < 300 ; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
[[Foo new] test];
NSLog(@"类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @"");
});
}
以上逻辑用真机测试会有卡死的几率,并非每次都会发生,但多尝试几次就会复现,伴随前后台切换,crash几率增大。
其中我们采用了 GCD 全局队列的方式来创建常驻线程,因为在创建时可能已经出现了全局队列的线程池满了的情况,所以 GCD 派发的任务,无法执行,而且我们把超时检测的逻辑放进了这个任务中,所以导致的情况就是,有很多任务的超时检测功能失效了。此时就只能依赖于服务端响应来结束该任务(服务端响应能结束该任务的逻辑在 Demo 中未给出),但是如果再加之服务端不响应,那么任务就永远不会结束。后续的网络请求也会就此 block 住,造成 crash。
如果我们把 GCD 全局队列换成 NSThread 的方式,那么就可以保证每次都会创建新的线程。
注意:文章中只演示的是超时 cancel runloop 的操作,实际项目中一定有其他主动 cancel runloop 的操作,就比如网络请求成功或失败后需要进行cancel操作。代码中没有展示网络请求成功或失败后的 cancel 操作。
Demo 的这种模拟可能比较极端,但是如果你维护的是一个像 AFNetworking 这样的一个网络库,你会放心把创建常驻线程这样的操作交给 GCD 全局队列吗?因为整个 APP 是在共享一个全局队列的线程池,那么如果 APP 把线程池沾满了,甚至线程池长时间占满且不结束,那么 AFNetworking 就自然不能再执行任务了,所以我们看到,即使是只会创建一条常驻线程, AFNetworking 依然采用了 NSThread 的方式而非 GCD 全局队列这种方式。
注释:以下方法存在于老版本AFN 2.x 中。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
正如你所看到的,没有任何一个库会用 GCD 全局队列来创建常驻线程,而你也应该
避免使用 GCD Global 队列来创建 Runloop 常驻线程。
队列(queue): 用来存放任务
队列不是线程 队列中存放的任务最后都要由线程来执行
队列的原则:(FIFO) First In First Out 先进先出 后进后出
队列类型
串行队列:Serial Dispatch Queue
存放顺序执行的任务
线程池只提供一个线程用来执行任务
一个任务执行完毕 在执行下一个任务
并发队列:Concurrent Dispatch Queue
存放想要同时并发执行的任务 可以开启多线程 具体数量由底层GCD负责
线程池可以提供多个线程来执行任务 具体数量由底层GCD负责
并发执行 性能高 执行顺序不固定 费电因为绝大多数会使用全局队列,全局队列本身就是并发队列
dispatch_queue_create
//串行队列
dispatch_queue_t SerialQueue;
//并发队列
dispatch_queue_t ConcurrentQueue;
//后面这个参数可以不写的 默认填NULL就是串行
SerialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
ConcurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
系统队列
系统提供了两个队列
** 主队列**:dispatch_get_main_queue
属于串行队列
负责调度主线程度的任务,没有办法开辟新的线程。
所以主队列下的任务不管是异步任务还是同步任务都不会开辟线程
任务只会在主线程顺序执行。
主队列异步任务:现将任务放在主队列中,但是不是马上执行,等到主队列中的其它所有除我们使用代码添加到主队列的任务的任务都执行完毕之后才会执行我们使用代码添加的任务。
主队列同步任务:容易阻塞主线程,所以不要这样写。原因:我们自己代码任务需要马上执行,但是主线程正在执行代码任务的方法体,因此代码任务就必须等待,而主线程又在等待代码任务的完成好去完成下面的任务,因此就形成了相互等待。整个主线程就被阻塞了。
全局队列: dispatch_get_global_queue
属于并发队列
一般情况下 并发任务都可以放在全局并发队列中
全局队列和并发队列的区别:
1 全局队列没有名字,但是并发队列有名字。有名字可以便于查看系统日志
2 全局队列是所有应用程序共享的。
3 在mrc的时候,全局队列不用手动释放,但是并发队列需要。
dispatch_queue_t
dispatch_queue_t mymainQueue;
dispatch_queue_t myglobalQueue;
mymainQueue = dispatch_get_main_queue();
myglobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/**参数说明:
参数1:代表该任务的优先级,默认写0就行,不要使用系统提供的枚举类型,因为ios7和ios8的枚举数值不一样,使用数字可以通用。
DISPATCH_QUEUE_PRIORITY_HIGH 2
DISPATCH_QUEUE_PRIORITY_DEFAULT 0
DISPATCH_QUEUE_PRIORITY_LOW (-2)
DISPATCH_QUEUE_PRIORITY_BACKGROUND (-32768)
参数2:苹果保留关键字,一般写0或NULL
所以也可以写为myglobalQueue = dispatch_get_global_queue(0, 0);
*/
同步异步
sync
同步运行:
如果是同步执行 队列会等任务结束后 再调度后续的任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block)
;
async
异步运行:
dispatch_sync(dispatch_queue_t queue, ^(void)block ) *dispatch_block_t
就是无返回值 无参数的block
常用用法介绍
1.经典用法(子线程下载(耗时操作),主线程刷新UI)
dispatch_async(dispatch_get_global_queue(0,0), ^{
//执行耗时的异步操作…
dispatch_async(dispatch_get_main_queue(), ^{
//回到主线程,执行UI刷新操作
});
});
2.GCD的延时执行
延时是延时任务加入到队列的时间 不是延时任务执行的时间
//1.延时不是一定时间后执行相应的任务,而是一定时间后,将任务加入到队列中(队列里面再分配执行的时间)
//2.主线程 RunLoop 1/60秒检测时间,追加的时间范围 3s~(3+1/60)s
//3.在哪个线程执行,跟队列类型有关
//dispatch_after(一定时间后,将执行的操作加入到队列中)
//dispatch_time_t when 指定时间
/* NSEC_PER_SEC 秒
* NSEC_PER_MSEC 毫秒
* NSEC_PER_USEC 微秒
*/
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull*NSEC_PER_SEC);
dispatch_queue_t que = dispatch_queue_create("h", DISPATCH_QUEUE_SERIAL);
//1.第一种用法
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"第一种延时 code to be executed on the main queue after delay");
});
//2.第二种用法
//dispatch_function_t work 执行的c语言方法
dispatch_after_f(time, que, NULL, fun1);
//3.第三种用法
dispatch_after(time, que, ^{
NSLog(@"第三种延时 code to be executed on the main queue after delay");
});
------代表方法外的分割-----
void fun1(){
NSLog(@"第二种延时 code to be executed on the main queue after delay");
}
3.异步执行:
//dispatch_async +全局并发队列(可以开启多条线程)
//dispatch_async +自己创建的串行队列(开启一条线程)
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// something
});
dispatch_async(dispatch_get_main_queue(), ^{
// 主队列异步
});
一次性执行:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// code to be executed once
});
4.一次性执行和多次执行
/*
一次执行:dispatch_once
作用:在多线程的情况下,同样能够保证指定的代码块只被执行一次
快捷键:
应用场景:单例设计模式
*/
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"块代码只能执行一次");
});
/*
多次执行:dispatch_apply
以指定的次数将指定的Block加入到指定的队列中 并等待队列中操作全部完成.
当指定队列为串行时 有序单线程执行
当指定队列为并发队列时 多线程无序执行
*/
dispatch_queue_t q1 = dispatch_queue_create("a1", DISPATCH_QUEUE_CONCURRENT);/
dispatch_apply(3, q1, ^(size_t index) {
NSLog(@"重要的事情说三遍 第%zu遍 %@",index,[NSThread currentThread]);
});
5.dispatch_group分组
/**
作用:所有任务执行完成之后,统一通知用户
可以实现监听一组任务是否完成 完成后得到通知执行其他的操作
不包括延时任务 因为延时是任务放进队列的时间
*/
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t q1 = dispatch_queue_create("a1", DISPATCH_QUEUE_SERIAL);
dispatch_block_t t1 = ^{
NSLog(@"任务1");
};
dispatch_async(q1, t1);
dispatch_group_async(group, q1, ^{
NSLog(@"group1");
});
dispatch_group_async(group, q1, ^{
NSLog(@"group2");
});
dispatch_group_notify(group, q1, ^{
NSLog(@"end");
});
6.dispatch_barrier_async的使用
/*
dispatch_barrier_async是在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行
*/
dispatch_queue_t queue = dispatch_queue_create("aa", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"dispatch_async1");
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"dispatch_async2");
});
dispatch_barrier_async(queue, ^{
NSLog(@"dispatch_barrier_async1");
[NSThread sleepForTimeInterval:1];
NSLog(@"dispatch_barrier_async2");
});
dispatch_async(queue, ^{
NSLog(@"dispatch_async3");
});
7、dispatch_set_target_queue
使用dispatch_set_target_queue将多个串行的queue指定到了同一目标,那么着多个串行queue在目标queue上就是同步执行的,不再是并行执行。
dispatch_queue_t tq = dispatch_queue_create("tq",DISPATCH_QUEUE_SERIAL);
dispatch_queue_t q1 = dispatch_queue_create("q1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t q2 = dispatch_queue_create("q2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t q3 = dispatch_queue_create("q3", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(q1, tq);
dispatch_set_target_queue(q2, tq);
dispatch_set_target_queue(q3, tq);
dispatch_async(q1, ^{
NSLog(@"1 in %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:3.f];
NSLog(@"1 out%@",[NSThread currentThread]);
});
dispatch_async(q2, ^{
NSLog(@"2 in %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:2.f];
// dispatch_suspend(queue1);
NSLog(@"2 out %@",[NSThread currentThread]);
});
dispatch_async(q3, ^{
NSLog(@"3 in %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:1.f];
NSLog(@"3 out %@",[NSThread currentThread]);
});
GCD 死锁
-(void)viewDidLoad { [super viewDidLoad]; NSLog(@"1"); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2"); }); NSLog(@"3"); }
我们来一步一步解析。首先dispatch_sync
是同步的,它会造成一个后果那就是阻塞主线程,并且会一直会等待block
,而block
放入到了主线程队列dispatch_get_main_queue()
中,这是一个FIFA
队列,就是先进先出的队列。他在等待主线程执行。那么一目了然了。我用伪代码敲出来大家就知道了
MainThread { dispatch_get_main_queue(){ syncblock(); } }
MainThread
等待dispatch_sync,dispatch_sync
等待block
,block
等待mainquen
,maiden
等待MainThread
,而MainThread
等待dispatch_sync
。这样就形成了一个死循环。俗称DeadLock
死锁。
NSLog(@"1"); dispatch_async(dispatch_get_main_queue(), ^{ while (1) { NSLog(@"2"); } }); dispatch_async(dispatch_get_main_queue(), ^{ while (1) { NSLog(@"3"); } }); NSLog(@"4");
结果大家一运行就知道,
为什么第一个只输出1
和4
和2
。我们来解析问题的代码
这里面有两部异步block
但是放到了主线程队列里面,但是block
里面执行的是一个 while (1)
的死循环
我们一步一步解析。 主线程队列是个FIFO
也就是先进先出,先进的完了才执行第二个。而当
^{ while (1) { NSLog(@"2"); } }
放入到主线程队列后,它就永远执行不完,永远不会退出,所以
^{ while (1) { NSLog(@"3"); }
这个只能永远的等待,而这两个block又是异步的不会阻塞主线程所以主线程的输出依然木有问题。