GCD(1)--任务的顺序
Lazyloading Lv3

[toc]

GCD(1)任务的顺序

前言

GCD这个东西老生常谈了,我们在开发中还是比较常用,几乎我们每个项目中都会用到,但是频率却又不是特别高(先别反驳)。

因为可能只是个别对操作有特殊要求的地方使用,所以导致很多人对GCD熟悉但是不熟练,你可能会说我们的网络请求,图片加载等都用到了GCD实现多线程啊,是的不错,不过这些都是框架比如afn,sd等给我们封装好了的,真正我们主动自己去封装GCD进行操作的地方并不是很多,包括我自己之前对其中的部分也不是特别明白,后面我会模拟一下我们常用的操作,来让我们正确通过GCD使用多线程。

代码

国际惯例先看几个示例:

示例1

下面的代码打印顺序是什么样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatch_queue_t queue = dispatch_queue_create("lazy", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1--%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"2--%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
NSLog(@"4--%@",[NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"5--%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"6--%@",[NSThread currentThread]);
});

我们先不着急给答案,先给答案可能会先入为主,影响理解,先来分析,看看答案可能是什么
分析:

  1. queue是一个并发队列,任务12356都在这个队列,并发队列有多个任务出口,任务4在主队列,主队列是串行队列,只有一个任务出口
  2. queue中任务3是个同步任务,特点是不会开辟线程,且会立即执行任务并阻塞线程直到任务结束才执行下一个任务,1256是异步任务,可以开辟线程,不会立即执行会等待CPU调度,并且不会阻塞线程
  3. 通过前两步分析可知,queue中3不执行完,后续任务不会进入队列,那么56肯定在3之后入队列,而12入队列在3之前但是是异步任务,执行顺序不确定,所以12可能在3之前也可能在3之后,也可能3在12之间,那么其实1256整体之间的顺序就也不确定,4虽然不在queue中,但是3阻塞了主线程,所以4也在3之后
  4. 3结束后立马执行了4,56因为是异步任务顺序也不确定,但肯定在4之后

经过上面的分析可以得出几个可能的答案,分别是:

  • 123456
  • 324156
  • 312465
    …当然还有其他符合的答案,具体根据代码运行时CPU的状态决定,但都会符合上边4条分析的结论,下面我贴一下我这边打印出的几个结果
1
2
3
4
5
6
7
8
9
10
11
12
13
3--<_NSMainThread: 0x600002ed0140>{number = 1, name = main}
2--<NSThread: 0x600002e94280>{number = 3, name = (null)}
4--<_NSMainThread: 0x600002ed0140>{number = 1, name = main}
1--<NSThread: 0x600002ed1e40>{number = 6, name = (null)}
5--<NSThread: 0x600002ed1e40>{number = 6, name = (null)}
6--<NSThread: 0x600002e94280>{number = 3, name = (null)}
----------------------------分割线--------------------------------
3--<_NSMainThread: 0x600001250840>{number = 1, name = main}
2--<NSThread: 0x600001261c40>{number = 4, name = (null)}
4--<_NSMainThread: 0x600001250840>{number = 1, name = main}
5--<NSThread: 0x600001261c40>{number = 4, name = (null)}
6--<NSThread: 0x600001200040>{number = 6, name = (null)}
1--<NSThread: 0x600001209ec0>{number = 7, name = (null)}

你也可以亲自动手试一下

示例2

下面的代码打印顺序是什么样的

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_queue_create("lazy", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");

同样的我们先来分析,看看答案可能是什么

  1. queue是一个并发队列,234是异步任务在这个队列中,15在主队列(上边重复过的内容省略)
  2. 1肯定先打印,其他任务都在它之后,之后24加入队列,因为这是一个异步任务所以会先入队列等待执行,之后打印5
  3. 而当异步任务块被执行时(注意这个执行时),打印了2之后,又有一个异步任务3,此时将任务3加入队列等待执行,然后继续执行4,所以34肯定在2之后
  4. 那么24这个异步任务块结束后会从队列中取出3执行,为什么会等24之后执行3呢?因为3是在24执行时加入队列的任务,它入队列时候已经在执行打印24了,而3还处于等待执行状态
  5. 最后打印出来就是15243
1
2
3
4
5
1--<_NSMainThread: 0x600003224140>{number = 1, name = main}
5--<_NSMainThread: 0x600003224140>{number = 1, name = main}
2--<NSThread: 0x60000327cb00>{number = 6, name = (null)}
4--<NSThread: 0x60000327cb00>{number = 6, name = (null)}
3--<NSThread: 0x600003262e00>{number = 3, name = (null)}

那么下面再稍微变一点点,再看打印的是什么

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_queue_create("lazy", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");

我们来分析下

  1. queue是一个并发队列,24是异步任务在queue中,3是同步任务在queue中,15在主队列(上边重复过的内容省略)
  2. 1肯定先打印,其他任务都在它之后,之后24加入队列,因为这是一个异步任务所以会先入队列等待执行,之后打印5
  3. 当异步任务块被执行打印了2之后,此时有一个同步任务3加入队列,那同步任务的特点是不开辟线程并阻塞线程立即执行,所以3会打印,然后继续执行4
  4. 最后打印出的结果就是15234
1
2
3
4
5
6
1--<_NSMainThread: 0x600003a2c7c0>{number = 1, name = main}
5--<_NSMainThread: 0x600003a2c7c0>{number = 1, name = main}
2--<NSThread: 0x600003a3c5c0>{number = 6, name = (null)}
3--<NSThread: 0x600003a3c5c0>{number = 6, name = (null)}
4--<NSThread: 0x600003a3c5c0>{number = 6, name = (null)}

示例3

下面的代码打印顺序是什么样的

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_queue_create("lazy", DISPATCH_QUEUE_SERIAL);
NSLog(@"1--%@",[NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"2--%@",[NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
NSLog(@"4--%@",[NSThread currentThread]);
});
NSLog(@"5--%@",[NSThread currentThread]);

可以看出,这个和上边的示例也很相似,我们先来分析下

  1. queue在这里是串行队列,24是异步任务在queue中,3是同步任务在queue中,15在主队列(上边重复过的内容省略)
  2. 1肯定先打印,其他任务都在它之后,之后24加入队列,因为这是一个异步任务所以会先入队列等待执行,之后打印5
  3. 当异步任务块被执行时打印了2之后,此时有一个同步任务3加入队列,那同步任务的特点是不开辟线程并阻塞线程立即执行,可是queue是个串行队列只有一个出口也就是同一时间只能有一个任务在出队列,那么在24出了一半的时候,3要求立马出可是3在队列中处于24之后,就造成了拥挤等待,24执行完才能执行3,3要求立马执行,执行完才能执行4,互相等待就会crash
  4. 所以打印应该是152然后crash


这种相互等待的情况就叫死锁,所以我们在使用串行队列,并且有同步任务的时候,一定要注意,在同步任务执行前,一定确保之前的任务都执行完了

示例4

那么看了上一个死锁导致crash的例子后我们再来看一个

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_queue_create("lazy", DISPATCH_QUEUE_SERIAL);
NSLog(@"1--%@",[NSThread currentThread]);
dispatch_async(queue, ^{
// sleep(1);
NSLog(@"2--%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
NSLog(@"4--%@",[NSThread currentThread]);

国际惯例先来分析

  1. queue是个串行队列只有一个出口也就是同一时间只能有一个任务在出队列,后边的任务不能插队,23都在queue这个队列中,14在主队列(上边重复过的内容省略)
  2. 14都在主队列,1肯定先打印,234都在它后边,那4呢?4之前有个同步任务3,通过上边几个例子我们知道同步任务的特点,所以4肯定要等3执行完,那么4就在3之后,
  3. 再来看2,它虽然是一个异步任务,但是串行队列只有一个出口也就是同一时间只能有一个任务在出队列,先加入队列的任务即使不执行也会挡住出口,queue中3排在2之后,3要等待2执行完才能开始执行
  4. 那么这个例子和上边死锁的有什么区别?这例子中3虽然是同步任务但并不是在2执行时加入队列的,所以2执行时不需要考虑3的情况,也就不会造成相互等待,也就不会死锁
  5. 那么分析后最终应该是1234
1
2
3
4
1--<_NSMainThread: 0x600002538440>{number = 1, name = main}
2--<NSThread: 0x600002573d00>{number = 6, name = (null)}
3--<_NSMainThread: 0x600002538440>{number = 1, name = main}
4--<_NSMainThread: 0x600002538440>{number = 1, name = main}

如果对于第3条分析里2这个异步任务的顺序有疑问,可以吧代码中2上边的注释sleep(1)打开,看是不是即使2睡了一秒后,3还是会在他之后

示例5

再来看最后一个,下面的代码循环外会打印什么?

1
2
3
4
5
6
7
8
__block  NSInteger num = 0;
while (num < 10) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
num++;
NSLog(@"%ld--%@",num,[NSThread currentThread]);
});
}
NSLog(@"\n>>>>>>>>>%ld--%@\n",num,[NSThread currentThread]);

同样的我们先来分析

  1. 一个while循环,出循环的条件是num >= 10,并且是在主队列主线程执行
  2. 循环里边会将一个异步任务加入全局队列,任务是将循环外的变量num加1我们知道全局队列是一个并发队列,这里你也可以像上边的例子一样自己创建一个并发队列
  3. 并发队列有多个出口,允许多个任务同时执行,异步任务不会阻塞线程并且会先加入队列等待调度执行
  4. num只有在while循环里边才会进行+1,所以最少要加10次才会变成10,也就是最少会有10个异步任务被加入队列
  5. 但是通过第3步我们知道任务进去后不一定立马被执行,可能一个num++任务加入队列后还没来记得执行,while已经开始了下一次循环又将一个任务加入队列(取决于你的CPU状态),所以num可能被额外的++,而循环可能在某次++后跳出,但是队列中可能还有++任务未被调度
  6. 那么最低次数 10 + 额外次数 一定大于等于10,结果应该就是大于等于10
    来看一下打印
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1--<NSThread: 0x6000028a58c0>{number = 4, name = (null)}
4--<NSThread: 0x60000288a500>{number = 3, name = (null)}
5--<NSThread: 0x6000028bf680>{number = 8, name = (null)}
3--<NSThread: 0x6000028be7c0>{number = 6, name = (null)}
6--<NSThread: 0x6000028a58c0>{number = 4, name = (null)}
7--<NSThread: 0x60000288a500>{number = 3, name = (null)}
2--<NSThread: 0x6000028be4c0>{number = 7, name = (null)}
9--<NSThread: 0x6000028bf680>{number = 8, name = (null)}
8--<NSThread: 0x6000028dc2c0>{number = 5, name = (null)}
10--<NSThread: 0x6000028bf680>{number = 8, name = (null)}
11--<NSThread: 0x6000028a58c0>{number = 4, name = (null)}
12--<NSThread: 0x6000028be4c0>{number = 7, name = (null)}
16--<NSThread: 0x6000028bf680>{number = 8, name = (null)}
15--<NSThread: 0x6000028be7c0>{number = 6, name = (null)}
20--<NSThread: 0x6000028bf680>{number = 8, name = (null)}

>>>>>>>>>12--<_NSMainThread: 0x600002880000>{number = 1, name = main}

22--<NSThread: 0x6000028be7c0>{number = 6, name = (null)}
23--<NSThread: 0x6000028bf680>{number = 8, name = (null)}
17--<NSThread: 0x60000288a500>{number = 3, name = (null)}
......

可以看出循环外的log打印的是12,多余的打印就是额外加入队列的任务

后记

通过上边几个例子,我想你对于GCD的使用应该更加的熟悉了,但是这里只是列举了几种使用相对简单的场景,我们实际中可能需求更复杂,比如多个网络请求结束后执行某些操作,或者多个请求之间有依赖,或者对于某些数据要实现多读单写等,这里是抛砖引玉,先对基本使用有更深刻的理解,然后后续会结合更复杂的场景来介绍

  • 本文标题:GCD(1)--任务的顺序
  • 本文作者:Lazyloading
  • 创建时间:2022-05-26 17:50:54
  • 本文链接:https://lazy.wiki/posts/a3a7c4e/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!