您的位置:首页 > 移动开发 > IOS开发

iOS 【一篇文章引发的思考 —— 异步/同步/并发/串行】

2017-06-27 11:25 399 查看
逛到 stackoverflow 上面一篇提问,引起了我的深思,写了这么久的代码,反过头来去看最最基本的东西,却又觉得十分有趣。不废话了,直接上链接:

https://stackoverflow.com/questions/19180661/sync-dispatch-on-current-queue

引起我兴趣的主要是最下面的回复,我截图下来了,不能连外网的朋友可以看一下:



可以看到,上面有四个示例,其中的两个死锁了,另外两个正常运行。先来普及一下死锁的概念再解释原因。

死锁:在一个串行队列Q中执行任务A,可是此时又要在任务A中同步执行任务B,将任务B也是添加到了串行队列Q中,这样就会发生死锁。

就如同上面两个死锁的情况,外层 block 要等待执行完毕后才算完成,而此时在外层 block 内部又加了一个任务,而这个任务强制要同步执行,强制插入,这样一来就必须先执行内部的任务,可以此时线程队列被外层 block 占用着,必须等待外层 block 走到头才可以走新添加的任务。这样两个任务牵制彼此,都无法完成。

那么我们改一下 Situation 3 中的代码:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self goDoSomethingLongAndInvolved];
dispatch_sync(queue, ^{
NSLog(@"Situation 3");
});
});

将外层的队列改为并发队列,那么结果就是可以执行了。首先要明白的是这个外层的并发队列中只有一个任务,所以不存在什么串行、并发之分。又因为没有新的线程,所以内部先执行 goDoSome... 方法,再执行 NSLog 输出,最后执行外层 block 结束花括号。而内层的同步也就说明了必须先执行内层 block 内的代码,再继续向下执行。因为分属不同的队列,所以就让线程先过去执行内层 block,再回过头来执行外层的 block 剩下部分收尾就可以了。 

打个比方:一个工人(一条线程)去完成两条生产线(两个队列)的任务,正在生产线A(外层 block)工作,突然被叫到生产线B(内层 block)去工作,而且是必须去(同步执行),做完生产线B的任务,接着回来收尾生产线A的任务(收尾最后外层 block 的花括号)。

那 Situation 1 为什么没有发生死锁呢?答:因为内层的 block 是异步执行的,也就是在单线程并且同一串行队列中,内层 block 可以等待串行队列中其余的任务完成再来执行内层 block 内的任务,即等待外层 block 右花括号结束之后再来执行 NSLog。

Situation 4 同理。

Situation 1 和 Situation 4 的区别在于外层 block 是异步调用还是同步调用,但异步和同步是相对于同一线程下同级的任务而言的,所以说这里的任务只存在外层的 block 一个,所以说异步同步就没有区别了。

写了几个个小 Demo,大家可以分析一下。

第一个 demo,初步理解:

两个 VC,YellowVC push 到 GreenVC,在 GreenVC 中添加一个 pop 按钮。按钮的点击方法实现如下:

- (IBAction)btnClick2:(UIButton *)sender {
self.myBlock = ^{
NSLog(@"A"); // 任务A
sleep(3); // 任务B
NSLog(@"C"); // 任务C
};

[self.navigationController popViewControllerAnimated:true]; // 任务D

dispatch_async(dispatch_get_main_queue(), ^{
self.myBlock();
});
}

那么问题是任务 A、B、C、D执行顺序如何?(正解:D A B C)

那么我们再修改一下代码:

- (IBAction)btnClick2:(UIButton *)sender {
self.myBlock = ^{
NSLog(@"A"); // 任务A
sleep(3); // 任务B
NSLog(@"C"); // 任务C
};

[self.navigationController popViewControllerAnimated:true]; // 任务D
self.myBlock();
}


那么问题又来了,现在的任务调用顺序是什么呢?(正解:A B C D)

理由:上面两段代码,其区别在于 block 是否是异步调用的。而我们理解这两段程序首先要明白一点,那就是 pop 方法是异步调用的。如果 pop 不是异步调用的,那么你怎么可能在 pop 执行过后再去拿到 self 指针去调用其他的方法呢?pop 过后,self 可就出栈了,那样一来在 pop 之后调用 self 指针不就会报错了吗。

所以说第一段代码 pop 和 block 都是异步调用的,所以按照顺序执行。主线程中原本包含任务 btnClick2,所以按照顺序执行完 btnClick2 就可以了,而 pop 也不会受到影响(这里指卡顿)。第二段代码由于 pop 异步,并且主线程会将 pop 异步放在最后执行。那么主线程在执行 btnClick2 这个任务时,就会先执行完 myBlock 的赋值,然后将 pop 异步放在最后调用,再接着执行 myBlock 的代码,再接着执行
btnClick2 的右花括号。这样一来 btnClick2 任务就完全执行完毕,主线程空闲,此时再执行 pop 任务。所以说运行结果会先打印A,然后卡顿3s,然后打印C,最后再 pop。此时会造成 pop 的卡顿。

第二个 demo,有助于加深理解:

- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil);

NSLog(@"doSomething 1 %@", [NSThread currentThread]);
NSLog(@"doSomething 2 %@", [NSThread currentThread]);

dispatch_async(queue, ^{
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
});

dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
});

NSLog(@"doSomething 5 %@", [NSThread currentThread]);
}


那么上面的这 5 句打印,是以什么顺序执行的?

我们先为这段代码加上注释:

- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil); // 串行队列

// 主线程执行
NSLog(@"doSomething 1 %@", [NSThread currentThread]);
NSLog(@"doSomething 2 %@", [NSThread currentThread]);

// 添加到不同队列的任务,async 可以开线程立即执行,主线程不等待此任务执行完
dispatch_async(queue, ^{
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
});
// 主队列任务,async 不开线程,任务加入到主队列后面,主线程不等待此任务执行完
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
});
// 主线程执行
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
}

然后再来分析结果:

由于在主线程中顺序执行,所以 1 - 2 是首要先执行的,然后 3 会开线程并且启用全新的串行队列,所以 3 立即执行,但 3 开线程需要耗时。由于 4 主线程主队列异步等待执行,而 5 是主线程主队列正常顺序执行,所以 5 会在 4 之前执行。

所以得出结论:

1  2 肯定先顺序打印,3  4  5 均在 1  2 后面打印,4 一定在 5 之后打印,3 不一定在 1  2 之后的哪个位置打印。

第三个 demo,相信这个 demo 过后你就会对 异步/同步/串行/并发 理解的更加透彻了:

- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil);

NSLog(@"doSomething 1 %@", [NSThread currentThread]);
NSLog(@"doSomething 2 %@", [NSThread currentThread]);

dispatch_sync(queue, ^{
NSLog(@"doSomething 2.5 %@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"doSomething 6 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 7 %@", [NSThread currentThread]);
});

dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
});

NSLog(@"doSomething 5 %@", [NSThread currentThread]);
}


那么上面的这 8 句打印,是以什么顺序执行的?

我们还是先为这段代码加上注释:

- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil); // 串行队列

// 主线程执行
NSLog(@"doSomething 1 %@", [NSThread currentThread]);
NSLog(@"doSomething 2 %@", [NSThread currentThread]);

// 主线程等待sync完成任务再继续执行70行往下的代码。由于GCD优化,sync的任务会被当前线程(主线程)执行。
dispatch_sync(queue, ^{
NSLog(@"doSomething 2.5 %@", [NSThread currentThread]);
});

// 添加到不同队列的任务,async可以开线程立即执行,主线程不等待此任务执行完
dispatch_async(queue, ^{
// =====此时是子线程在执行代码=====
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
// 添加到同一个串行队列的异步任务,不开线程,任务加入到串行队列后面,当前线程不等待任务执行完
dispatch_async(queue, ^{
NSLog(@"doSomething 6 %@", [NSThread currentThread]);
});
// 当前线程先完成当前任务
NSLog(@"doSomething 7 %@", [NSThread currentThread]);
});
// 主队列任务,async不开线程,任务加入到主队列后面,主线程不等待此任务执行完
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
});
// 主线程执行
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
}

我们直接得出结果:
1  2  2.5 肯定率先执行,(3  6  7)在新的线程中执行,(3  6  7)整体顺序未知,5 肯定在 4 的前面执行。(3  6  7)内部肯定是 3  7  6 的顺序执行。

然后我们将 6 的地方改为:

dispatch_sync(queue, ^{
NSLog(@"doSomething 6 %@", [NSThread currentThread]);
});


那么结果会是怎样的?

直接得出结果:

主线程正常,而子线程死锁,App 还是崩溃了。

第四个 demo,如果你分析对了调用顺序,基本上这一部分就理解了:

- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil); //串行队列
dispatch_queue_t queue2 = dispatch_queue_create("darktest2", nil); //串行队列2

//主线程执行
NSLog(@"doSomething 1 %@", [NSThread currentThread]);

dispatch_sync(queue, ^{ // A
NSLog(@"doSomething 2 %@", [NSThread currentThread]);
dispatch_sync(queue2, ^{ // B
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
});

dispatch_async(queue, ^{ // C
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{ // D
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
dispatch_sync(queue2, ^{ // E
NSLog(@"doSomething 6 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 7 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 8 %@", [NSThread currentThread]);
});

NSLog(@"doSomething 9 %@", [NSThread currentThread]);
});

//主线程执行
NSLog(@"doSomething 10 %@", [NSThread currentThread]);
}

直接给出答案:1  2  3  9  (4) 10 (4)  5  6  7  8
其中 4 的打印位置不确定,如果你也得到的这个结果,那么恭喜你。在这里不做解释。不明白的可以留言。

最后得出我们本文的结论:

异步、同步、串行、并发 这些名词的概念都不能脱离 线程、队列 来单独描述。单纯去解释一个名词在我看来是没有意义的。大家通过 demo 可以理解了就好。

本文感谢:darkhandz 提供 demo,感谢我俩两天如同浆糊般的讨论。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  iOS 同步 异步 串行 并发