前言
标题可能有点令人费解,解释一下。众所周知,GCD编程是面向队列和任务的,无需关心线程的创建和维护。GCD中有两种队列和两种任务,不同队列和不同任务组合起来往往就容易被绕晕。本文就队列和任务的四种组合情况,结合任务间嵌套进行分析。
系好安全带
队列
GCD中有两种队列:串行队列和并行队列。队列里面存放要执行的任务,既然是队列,那么一定遵循先进先出的原则。比如往队列中先添加A任务再添加B任务,则一定是A任务先执行,B任务再执行。不管串行队列还是并行队列,都是要遵守这个原则的。
而串行队列和并行队列的区别在于,串行队列中,一定是先取A任务并执行,等到A任务执行完毕后才取B任务并执行。并行队列同样是先取A任务再取B任务,但不必等到A执行完再取B,而是在A任务还在执行的时候,就可以取出B任务并执行。
注意:千万不要认为,并行队列可以不顾任务添加顺序而"并行"地取多个任务,队列就应该有个队列的样子o(╯□╰)o-
任务
GCD有两种任务:同步任务和异步任务。这俩的区别在于,是否阻塞当前线程。什么意思呢?向某个队列添加任务,这个操作,肯定是在某个线程里执行的,比如主线程。如果是同步任务,则一定是先把这个任务执行完了,才可以继续往下执行,比如下面代码:- (void)viewDidLoad { [super viewDidLoad]; /* 向队列中添加任务的代码 */ NSLog("GCD test"); }
如果是同步任务,那只能是等这个任务执行完之后,才会来到NSLog
那里,即使这个任务有好多循环,进行了大量复杂的计算,也只能是这样,谁叫这是个同步任务呢。若是异步任务,那执行完添加任务的那部分代码后,直接往后走,那个异步任务在合适的时候再执行,一般那个异步任务会在一个新的线程里执行。
小结一下,同步任务会阻塞当前线程,一定是在当前线程下执行,GCD不会开启新的线程;而异步任务,不会阻塞当前线程,当前线程会继续执行后面的代码,一般这个异步任务会在一个新的线程中执行,但这不是绝对的,并不是说添加了一个异步任务后GCD就会开启一个新的线程,后面有例子。
发车啦
下面将对队列和任务组合,结合任务的嵌套进行分析
串行队列的同步任务
这种情况最简单,执行的结果是可预测的,比如下面代码:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t serialQueue = dispatch_queue_create("com.test.serial", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
NSLog(@"inside -- %@",[NSThread currentThread]);
});
NSLog(@"outside -- %@",[NSThread currentThread]);
}
运行结果如下:
inside -- <NSThread: 0x7fcfe9704e70>{number = 1, name = main}
outside -- <NSThread: 0x7fcfe9704e70>{number = 1, name = main}
由于是同步任务,会先执行这个同步任务再往下执行,实际上跟下面这个效果是一样的:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"inside -- %@",[NSThread currentThread]);
NSLog(@"outside -- %@",[NSThread currentThread]);
}
呵呵。
等等,说好的将队列和任务的组合,怎么只讲任务了。如果要组合起来的话,就使用任务的嵌套吧。修改一下上面的代码
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t serialQueue = dispatch_queue_create("com.test.serial", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
NSLog(@"outside -- %@",[NSThread currentThread]);
dispatch_sync(serialQueue, ^{
NSLog(@"inside -- %@",[NSThread currentThread]);
});
});
}
注意:现在就不在主线程里打印日志了,注意力全部放在队列和任务的组合上
运行结果如下:
outside -- <NSThread: 0x7fe7c2700360>{number = 1, name = main}
如果outside
的那个NSLog
写在当前任务的最后,也就是放在第二个同步任务的后面,运行结果是什么都不会打印。
好,现在使用刚才讲的队列和任务的特点分析一下。
首先外层的任务是一个同步任务,会阻塞当前线程,这里的当前线程就是主线程了,因为是在viewDidLoad
里面嘛。那就执行这个同步任务呗,由于该任务是添加在serialQueue
队列中,所以从这个队列里面取出这个同步任务并执行,执行到outside
的那个log
时打印出了当前线程,结果是主线程,因为阻塞了主线程嘛,自然就在主线程中运行了。
接着往下执行,又往serialQueue
中添加一个任务,这又是个同步任务,阻塞当前线程,要执行完了这个任务才能接着往下执行。那就把这个任务取出来,赶快执行掉好往下走啊。很遗憾,serialQueue
是一个串行队列,现在正在执行第一个同步任务,就是有outside
打印的那个同步任务,所以serialQueue
取不出inside
那个任务。所以,inside
这个任务要等outside
执行完才能执行。另一方面,由于第二个任务(inside
)是同步任务,一定要执行完这个同步任务才能往下走,所以outside
要等inside
这个任务执行完了,才能往下走,才能走完,这个任务才得以结束。所以outside
要等inside
执行完才能执行完。这两个任务就这样互相掐着,永远不能执行或者执行完毕。更悲惨的是,这两个任务阻塞的是主线程,那主线程就没办法再继续走下去了。解决的方法,可以将两个任务添加到不同的队列中,两个队列取任务互不干扰,就不会出现“互掐”的情况了。或者将队列改成并行队列,或者将第二个任务改成异步任务,这些都会在下面的组合情况中看到。
这也就解释了,为什么在主队列上只能添加异步任务。首先主队列是一个串行队列,其次程序启动后,主队列中一直有一个任务,不断地监听用户输入及各种事件。所以如果在主线程中,往主队列中添加同步任务,则这个同步任务会阻塞主线程,而且永远取不出来,应用中断不能进行下去。如果是在其他线程向主队列添加同步任务,则主线程不会影响,但这个同步任务是永远不会执行到了。
小结:同一个串行队列不能嵌套同步任务,注意是同一个哦。
串行队列的异步任务
接刚才的例子,如何使用异步任务解决刚才的问题。刚才的问题是两个任务都依赖着彼此,所以都不能执行或者执行完毕。所以只要解除其中任意一条就行。就本节而言,因为要使用异步任务,所以解决方式就是不让outside
依赖inside
的执行,也就是说inside
这个任务改成异步任务,代码只需要加一个字母😳
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t serialQueue = dispatch_queue_create("com.test.serial", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
NSLog(@"outside -- %@",[NSThread currentThread]);
dispatch_async(serialQueue, ^{
NSLog(@"inside -- %@",[NSThread currentThread]);
});
});
}
把inside
的那个任务由sync
改成async
,打印结果如下:
outside -- <NSThread: 0x7ff57940a560>{number = 1, name = main}
inside -- <NSThread: 0x7ff5796102a0>{number = 2, name = (null)}
可以看到,inside
任务在一个新的线程,number = 2
中执行。
值得注意的是,尽管inside
任务是在一个新的线程中执行的,但inside
还是要等到outside
执行完才能执行,所以考虑以下代码:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t serialQueue = dispatch_queue_create("com.test.serial", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
NSLog(@"outside -- %@",[NSThread currentThread]);
dispatch_async(serialQueue, ^{
NSLog(@"inside -- %@",[NSThread currentThread]);
for (NSInteger idx = 1; idx <= 100; idx++) {
NSLog(@"inside -- %@",@(idx));
}
});
for (NSInteger idx = 1; idx <= 100; idx++) {
NSLog(@"outside -- %@",@(idx));
}
});
}
运行结果是可预测的,一定是outside
先打印完100个数据后,inside
再开始打印,因为是串行队列,所以一定要outside
执行完后才执行inside
任务。
outside -- <NSThread: 0x7f975a509d40>{number = 1, name = main}
outside -- 1
outside -- 2
...
outside -- 100
inside -- <NSThread: 0x7f975a409790>{number = 2, name = (null)}
outside -- 1
outside -- 2
...
outside -- 100
那如果外面是异步任务,里面是同步任务呢?外面的异步任务只是不会阻塞主线程,会开启一个新的线程,但内层的同步任务就会阻塞这个新建的线程,但由于又是串行队列,永远取不到这个同步任务。所以外层的异步任务永远不会执行完,内层的同步任务永远不会被取出并执行。相对第一种情况(两个同步任务)而言,只是没有阻塞主线程而已。
如果两个任务都是异步任务,则两个任务都会顺利地执行。这里需要注意的是,外层的异步任务,GCD会开启一个新的线程,而内层的任务,虽然是异步任务,但GCD不会开启新的线程,而是在与外层任务相同的线程中执行。因为这是个串行队列,必须外层先执行完才能轮到内层执行,所以没有必要再开一个新的线程执行,并行队列就不一样咯。这就是上面所说的,异步任务不一定会使GCD开启一个新的线程。代码和运行结果如下:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t serialQueue = dispatch_queue_create("com.test.serial", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
NSLog(@"outside -- %@",[NSThread currentThread]);
dispatch_async(serialQueue, ^{
NSLog(@"inside -- %@",[NSThread currentThread]);
for (NSInteger idx = 1; idx <= 100; idx++) {
NSLog(@"inside -- %@",@(idx));
}
});
for (NSInteger idx = 1; idx <= 100; idx++) {
NSLog(@"outside -- %@",@(idx));
}
});
}
outside -- <NSThread: 0x7fd723db43c0>{number = 2, name = (null)}
outside -- 1
outside -- 2
...
outside -- 100
inside -- <NSThread: 0x7fd723db43c0>{number = 2, name = (null)}
inside -- 1
inside -- 2
...
inside -- 100
可见outside
和inside
都是在number = 2
的线程中执行的。
并行队列的同步任务
刚才说了,只要解除两个任务中的任意一条依赖就行。使用串行队列的异步任务是解除outside
对inside
的依赖,使得outside
可以顺利结束。现在试着解除inside
对outside
的依赖,要解除这种依赖,就要在outside
还在执行的时候,就可以取出inside
,并行队列正好可以解决这个问题。代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(concurrentQueue, ^{
NSLog(@"outside -- %@",[NSThread currentThread]);
dispatch_sync(concurrentQueue, ^{
NSLog(@"inside -- %@",[NSThread currentThread]);
});
});
}
没什么大改动,只是把队列改成了并行队列。如果再加上刚才的循环呢?
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(concurrentQueue, ^{
NSLog(@"outside -- %@",[NSThread currentThread]);
dispatch_sync(concurrentQueue, ^{
NSLog(@"inside -- %@",[NSThread currentThread]);
for (NSInteger idx = 1; idx <= 100; idx++) {
NSLog(@"inside -- %@",@(idx));
}
});
for (NSInteger idx = 1; idx <= 100; idx++) {
NSLog(@"outside -- %@",@(idx));
}
});
}
因为inside
任务是同步任务,阻塞了outside
的执行,所以要先执行完inside
,outside
才能接着执行,所以这种情况,运行结果是可预测的。结果如下:
outside -- <NSThread: 0x7feef8707490>{number = 1, name = main}
inside -- <NSThread: 0x7feef8707490>{number = 1, name = main}
inside -- 1
inside -- 2
...
inside -- 100
outside -- 1
outside -- 2
...
outside -- 100
并行队列的异步任务
考虑以下三种情况。
外层异步任务,内层同步任务,代码
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
NSLog(@"outside -- %@",[NSThread currentThread]);
dispatch_sync(concurrentQueue, ^{
NSLog(@"inside -- %@",[NSThread currentThread]);
for (NSInteger idx = 1; idx <= 100; idx++) {
NSLog(@"inside -- %@",@(idx));
}
});
for (NSInteger idx = 1; idx <= 100; idx++) {
NSLog(@"outside -- %@",@(idx));
}
});
}
这种情况,要是放在串行队列中,这两个任务就不能顺利执行了。但因为是并行队列,当外层的异步任务还在执行的时候,是可以取出内层的同步任务的。注意,虽然内层的同步任务阻塞了当前线程,但并行队列可以将其取出并在当前线程中执行,执行完后,外层的异步任务再继续往下执行。所以运行结果是可预测的。
outside -- <NSThread: 0x7ff388f10630>{number = 2, name = (null)}
inside -- <NSThread: 0x7ff388f10630>{number = 2, name = (null)}
inside -- 1
inside -- 2
...
inside -- 100
outside -- 1
outside -- 2
...
outside -- 100
外层同步任务,内层异步任务,代码:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(concurrentQueue, ^{
NSLog(@"outside -- %@",[NSThread currentThread]);
dispatch_async(concurrentQueue, ^{
NSLog(@"inside -- %@",[NSThread currentThread]);
for (NSInteger idx = 1; idx <= 100; idx++) {
NSLog(@"inside -- %@",@(idx));
}
});
for (NSInteger idx = 1; idx <= 100; idx++) {
NSLog(@"outside -- %@",@(idx));
}
});
}
外层的同步任务,在主线程中运行。当执行到内层的任务时,由于是异步任务,所以不会阻塞当前线程,添加完任务后继续往后面走,而且由于是并行队列,所以这个异步任务在外层同步任务执行完之前,可以被取出并执行,当然,是在一个新的线程里面执行,也就是说这两个任务在不同的线程中并行地在执行,所以结果是不可预测的。我这里的运行结果是这样的:
outside -- <NSThread: 0x7ff45bf07400>{number = 1, name = main}
outside -- 1
inside -- <NSThread: 0x7ff45bdb1a40>{number = 2, name = (null)}
outside -- 2
inside -- 1
outside -- 3
inside -- 2
...
外层和内层任务都是异步任务。这种情况相对上面来说是最复杂的,嗯。代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
NSLog(@"outside -- %@",[NSThread currentThread]);
dispatch_async(concurrentQueue, ^{
NSLog(@"inside -- %@",[NSThread currentThread]);
for (NSInteger idx = 1; idx <= 100; idx++) {
NSLog(@"inside -- %@",@(idx));
}
});
for (NSInteger idx = 1; idx <= 100; idx++) {
NSLog(@"outside -- %@",@(idx));
}
});
}
其实,相对于外层同步内层异步的情况,只是外层任务变成了异步,那就不会阻塞主线程了,而是在一个新的线程中运行,当走到内层的异步任务时,不会阻塞这个新建的线程,又由于这是并行队列,所以可以把内层的异步任务取出,所以,两个任务又是在不同的线程中并行的执行,只不过外层的异步任务不是在主线程中执行。运行结果同样是不可预测的:
outside -- <NSThread: 0x7f9d2a535470>{number = 2, name = (null)}
outside -- 1
outside -- 2
inside -- <NSThread: 0x7f9d2a5386c0>{number = 3, name = (null)}
outside -- 3
inside -- 1
outside -- 4
inside -- 2
...
可以看到,两个任务分别在不同的线程(number = 2, number =3
)中执行。
总结
- 同一串行队列下,任务的嵌套,内部只能嵌套异步任务,这也就是主队列中只能添加异步任务的原因;
- 同一串行队列中的任务运行结果总是可预测但可能造成“互掐”的情况,导致某些任务不能正确执行;
- 并行队列中的任务,不管如何组合都不会造成“互掐”的情况,但某些运行结果不可预测;
- 牢记队列和任务各自的职责和特点,就不容易绕晕了。