GCD

1.简介

GCD是在线程池模式基础上开发的一套多核编程的较新的解决方案:

  • 底层由C语言实现,API使用时简洁明了;
  • 为多核处理器而设计,会自动利用更多的CPU内核进行运算,执行效率高;
  • 自动管理线程生命周期,无需手写线程管理的代码,只需告诉它我们想要执行的任务。

2.同步与异步

同步和异步是将任务以 block 形式提交到指定队列中时所使用的两种方式。

1
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

dispatch_sync用于同步地执行任务。函数会一直等到 blcok 执行完才返回,这会阻塞当前线程。

1
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

dispatch_async用于异步地执行任务。函数在提交完任务后不等待 block 执行完就立刻返回了,不会阻塞当前线程。

3.队列

1.串行与并发队列

当有多个任务时,可以将它们放在队列中执行,队列分为串行和并发两种:

串行队列:任务是按照FIFO的顺序执行的,先提交的任务先执行,后提交的任务须等前一个任务执行完成才能开始。对于不同的串行队列,系统会为它们创建不同的线程来执行任务。

并发队列:任务也是按照FIFO的顺序执行,不同的是,任务不是必须等到前一个执行完才开始,同一时间可能会有多个任务在执行。GCD 会动态分配多条线程来执行这些任务,具体几条线程取决于当前内存状况和线程池中线程数等因素。

#特殊的串行队列: main_queue

1
2
3
//主队列本身是系统提供的一种特殊的串行队列
//放在主队列中的任务都会在主线程中执行
dispatch_queue_t mainQueue = dispatch_get_main_queue();

#特殊的并发队列: global_queue

1
2
3
4
//全局队列是系统提供的一种特殊并发队列
//第一个参数指定queue的优先级,分为HIGH\DEFAULT\LOW\BACKGROUND四种;
//第二个参数,目前只能为0或NULL;
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_ PRIORITY_DEFAULT,0);

2.任务+队列 的组合

. 并发队列 串行队列 主队列
同步 不能开启新线程,串行执行任务 不能开启新线程,串行执行任务 不能开启新线程,串行执行任务
异步 可以开启新线程,并发执行任务 可以开启新线程,串行执行任务 不能开启新线程,串行执行任务

注释:不能开启新线程,就要继续在当前线程中运行。

#示例3.2.1:

1
2
3
4
5
6
7
8
9
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(concurrentQueue, ^{
NSLog(@"+++Thread:%@,main?:%d",[NSThread currentThread],[NSThread isMainThread]);
});
return YES;
}

启动后输出结果为:

1
+++Thread:<NSThread: 0x6000000dca40>{number = 1, name = main},main?:1

分析:示例中向并发队列concurrentQueue中同步的提交了一个打印日志的任务,虽然并发队列具有开辟新线程的能力,但是结合上dispatch_sync之后,就不行了,任务会继续运行在当前的主线程上。输出结果中“name = main” 且 “main?:1”也印证了这一点。

再注意:可以开启新线程,也不一定就真的会开辟新线程执行新的任务,系统要根据线程池中的状况来决定是开辟新线程,还是继续在当前空闲的线程上执行任务。

#示例3.2.2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
//step.1
dispatch_async(concurrentQueue, ^{//具有开启新线程的能力
NSLog(@"+++Thread1:%@,main?:%d",[NSThread currentThread],[NSThread isMainThread]);
//step.2
dispatch_async(concurrentQueue, ^{//具有开启新线程的能力
NSLog(@"+++Thread2:%@,main?:%d",[NSThread currentThread],[NSThread isMainThread]);
//step.3
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"+++Thread3:%@,main?:%d",[NSThread currentThread],[NSThread isMainThread]);
});
});
//step.4
// for (int i = 0; i< 0xffffffff; i++){//for循环很多次,模拟大量运算
// }
});
return YES;
}

运行后日志为:

1
2
3
+++Thread1:<NSThread: 0x600003bd5500>{number = 3, name = (null)},main?:0
+++Thread2:<NSThread: 0x600003bd5500>{number = 3, name = (null)},main?:0
+++Thread3:<NSThread: 0x600003bb1900>{number = 1, name = main},main?:1

step1处,异步+并发,已具备开启新线程的能力,从日志来看 Thread1 “main?:0”,已经不在主线程,开启了新线程;

step2处,异步+并发,也具备开启新线程的能力,从日志来看线程 Thread2 的内存地址与 step1 时一样,说明继续运行在当前线程上,并没有开辟新线程~

不过,如果把step4处的注释去掉,模拟一次复杂运算,则输出结果就不一样了:

1
2
3
+++Thread1:<NSThread: 0x600000c98ec0>{number = 3, name = (null)},main?:0
+++Thread2:<NSThread: 0x600000cab140>{number = 4, name = (null)},main?:0
+++Thread3:<NSThread: 0x600000cfd900>{number = 1, name = main},main?:1

Thread2Thread1的地址已经不一样了。这是因为Thread1中有大量运算,Thread2要开启一个新线程执行任务。

所以,同/异步 + 串/并行的组合,只是说明了任务所在队列,任务的提交方式和是否具备开辟新线程的能力。当具备开辟新线程的能力时,是否真的开辟新线程要由线程池的状况来确定

3.死锁问题

在当前串行队列中再同步地提交一个新任务到队列中会造成死锁!

#示例3.3.1:

主队列中使用“同步+主队列”的组合,造成死锁的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
//step.1
NSLog(@"++++任务开始");
dispatch_queue_t mainQueue = dispatch_get_main_queue();

//step.2
dispatch_sync(mainQueue, ^{
NSLog(@"++++处理任务4");
//do something
});

//step.3
NSLog(@"++++任务结束");
}

上面的代码行后,只会输出“++++任务开始”,并卡死在dispatch_sync这一行。这是因为:viewDidAppear是在主队列中,这是一个串行队列,里面的任务按照FIFO的顺序依次执行。整个代码块中大的任务顺序为123,但是执行到 step2 时,同步地向主队列中追加了一个任务4,因此任务顺序变为1234。dispatch_sync提交任务的特点是会阻塞当前线程,并立刻开始执行其中的任务。问题就来了,主线程本来要按顺序执行1、2、3,结果执行到 2 时被强行阻塞,并被要求立刻去执行任务4。而串行队列中任务4位于3之后,不能在3尚未执行时先执行4。所以形成一种局面:执行到step2时,串行主队列在等待任务4完成,任务4又在等待主队列完成任务3,从而形成了死锁问题。

#示例3.3.2:

1
2
3
4
5
6
7
8
9
10
//当前在主线程
dispatch_queue_t serialQueue = dispatch_queue_create("seria", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{//block1
NSLog(@"1");
dispatch_sync(serialQueue, ^{// block2
NSLog(@"2");
});
NSLog(@"3");
});
NSLog(@"4");

上述示例中会先打印日志“4”,后打印“1”,之后在 dispatch_sync 这一行卡死。这其实与示例1 类似:因为 block1 是异步执行,所以函数立刻返回并打印“4”;但是 block1 内任务是在一个串行队列中,执行到dispatch_sync处时线程被阻塞,并被要求立刻在当前串行队列中再执行追加到尾部的新任务 block2。这当然也行不通,因为当前线程已被block2阻塞,block2又在等待队列先完成任务3。这样也造成了相互等待的情况,形成了死锁。

4.线程间的通信

通常为了避免UI阻塞,我们可以在异步线程中做一些耗时的任务,完成后回到主线程中更新UI。

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//图片的网络路径
NSURL *url = [NSURL URLWithString:@"xxx.jpg"];
//加载图片
NSData *data = [NSData dataWithContentsOfURL:url];
//生成图片
UIImage *image = [UIImage imageWithData:data];

//回到主线程,刷新UI,显示图片。
dispatch_async(dispatch_get_main_queue(), ^{
[self.imageView setImage:image];
});
});

例子在主线程中调用,dispatch_async配合全局并发队列,开辟了一个新线程用来加载图片资源;加载完成后再通过dispatch_get_main_queue返回到主队列中更新视图。因为主队列默认在主线程中,所以更新视图的任务会在主线程中进行。

5.线程与队列

线程队列是两个不同的概念~

队列是一种任务组织方式,任务被以 block 的形式提交到队列中,并最终在某个线程上执行,具体在哪个线程上执行是由队列类型提交任务时的方式决定的。如#3.2小节中列出的那样,如果是同步的方式提交任务,则任务会继续在当前线程上执行;如果是异步+并发队列的方式提交的任务,那么内核会根据系统资源创建新线程来执行任务。

线程是 CPU 调度和分派任务的基本单位,一个线程中可以执行多个队列中的任务。线程是可以重复利用的,这由线程池和系统资源状况来决定。

主线程不等于主队列。主线程只有一个,主线程上既可以执行主队列中的任务,也可以执行全局并发队列或你自定义队列中的任务。

#示例:主线程执行非主队列的任务

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSLog(@"+++1.主线程?:%@",[NSThread isMainThread] ? @"YES" : @"NO" );//YES

dispatch_sync(dispatch_get_global_queue(0, 0), ^{//同步的提交任务,模拟任务还在当前线程执行
NSLog(@"+++2.主线程?:%@",[NSThread isMainThread] ? @"YES" : @"NO" );//YES
});

return YES;
}

主队列的任务一定在主线程中执行,全局并发队列或自定义队列中的任务可以在子线程中执行。

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSLog(@"+++1.主线程?:%@",[NSThread isMainThread] ? @"YES" : @"NO" );//YES

dispatch_async(dispatch_get_main_queue(), ^{//虽然异步提交主队列中的任务,但任务还是在主线程中执行
NSLog(@"+++2.主线程?:%@",[NSThread isMainThread] ? @"YES" : @"NO" );//YES
});

return YES;
}

很多跟 UI 相关的操作,一般都会放到main_queue主队列中进行,比如常见的AFN从子线程回到主队列更新图片。这是因为UIKit本身不是线程安全的,这些操作需要在系统提供的主队列这么一个串行队列中进行,保证操作的顺序和完整性。

4.栅栏

1.dispatch_barrier_async

作用:让队列中排在它前面任务先执行完毕,再执行自己,最后再执行后续任务。

场景:向并发队列中加入5个异步任务:1、2、3、4、5。由于是异步+并发,所以任务实际执行时并不一定按照 12345 的顺序来。如果我们要求执行完任务 3 之后才能执行任务 4 和 5,此时就可以使用栅栏这个功能。

#示例4.1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)barrier{
//自定义并发队列
dispatch_queue_t cQueue = dispatch_queue_create("Name1",DISPATCH_QUEUE_CONCURRENT);

dispatch_async(cQueue, ^(){
NSLog(@"++++任务1:%@",[NSThread currentThread]);
});
dispatch_async(cQueue, ^(){
NSLog(@"++++任务2:%@",[NSThread currentThread]);
});
dispatch_barrier_async(cQueue, ^(){
NSLog(@"++++任务3栅栏:%@",[NSThread currentThread]);
});
NSLog(@"++++1");
dispatch_async(cQueue, ^(){
NSLog(@"++++任务4:%@",[NSThread currentThread]);
});
NSLog(@"++++2");
dispatch_async(cQueue, ^(){
NSLog(@"++++任务5:%@",[NSThread currentThread]);
});
}

日志:

1
2
3
4
5
6
7
++++1
++++2
++++任务2:<NSThread: 0x600003453f00>{number = 4, name = (null)}
++++任务1:<NSThread: 0x600003455f00>{number = 3, name = (null)}
++++任务3栅栏:<NSThread: 0x600003455f00>{number = 3, name = (null)}
++++任务4:<NSThread: 0x600003455f00>{number = 3, name = (null)}
++++任务5:<NSThread: 0x600003453f00>{number = 4, name = (null)}

从日志来看,任务的执行顺序正常:

  • 栅栏之前的任务1和2先执行;
  • 随后是栅栏本身的任务3;
  • 最后是栅栏之后的任务4和5。

注意:栅栏只是用来控制同一并发队列中栅栏前后任务的整体执行顺序;至于栅栏前面的任务与后面的任务它们具体在哪个线程执行,这不是栅栏能决定的。

结合日志中任务所在线程的内存地址来看:

  • 栅栏之前的任务1任务2并没在同一个线程中执行;
  • 栅栏之后的任务4任务5也没有在同一个线程中执行;
  • 甚至栅栏前的任务1和栅栏任务3及栅栏之后的任务4使用了同一个线程;

所以,栅栏后的任务具体是在哪个线程上执行,还是要看这些任务的提交方式和队列类型,之后交给线程池来决定。

但是,如果任务都是通过栅栏提交的,那么栅栏就能保证它们按照来时的顺序先后执行,且是在同一线程上:

#示例4.2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)barrier2{
//自定义并发队列
dispatch_queue_t cQueue = dispatch_queue_create("Name1",DISPATCH_QUEUE_CONCURRENT);

dispatch_async(cQueue, ^(){
NSLog(@"++++任务1:%@",[NSThread currentThread]);
});
dispatch_async(cQueue, ^(){
NSLog(@"++++任务2:%@",[NSThread currentThread]);
});
dispatch_barrier_async(cQueue, ^(){
NSLog(@"++++任务3栅栏:%@",[NSThread currentThread]);
});
NSLog(@"++++1");
dispatch_barrier_async(cQueue, ^(){ //注意,这里换成了栅栏,不仅仅是异步了
NSLog(@"++++任务4:%@",[NSThread currentThread]);
});
NSLog(@"++++2");
dispatch_barrier_async(cQueue, ^(){ // 这里也是
NSLog(@"++++任务5:%@",[NSThread currentThread]);
});
}

日志:

1
2
3
4
5
6
7
++++1
++++2
++++任务1:<NSThread: 0x60000132cb80>{number = 5, name = (null)}
++++任务2:<NSThread: 0x60000132a840>{number = 6, name = (null)}
++++任务3栅栏:<NSThread: 0x60000132a840>{number = 6, name = (null)}
++++任务4:<NSThread: 0x60000132a840>{number = 6, name = (null)}
++++任务5:<NSThread: 0x60000132a840>{number = 6, name = (null)}

可以看到,提交到栅栏中的任务3、4、5是按照提交时的顺序执行的,且在同一个线程中。

另外,该函数需要同dispatch_queue_create()生成的并发队列一起用。如果示例4.1中换成全局队列则效果如下:

1
2
3
4
5
6
7
++++1
++++任务3栅栏
++++任务2
++++任务1
++++2
++++任务4
++++任务5

任务1、2在栅栏3之后执行了,栅栏并未起效!!!,所以实际中须注意这一点~

2.dispatch_barrier_sync

将示例4.1中栅栏替换为dispatch_barrier_sync后,日志如下:

1
2
3
4
5
6
7
++++任务1
++++任务2
++++任务3栅栏
++++1
++++2
++++任务4
++++任务5

对比日志,两种情况下:

1、任务都是按照栅栏3之前的先执行,再是3本身,最后是45;

2、“++++数字”日志都是按照12的顺序执行,只是在异步栅栏中“++++数字” 与 “++++任务x”的打印了顺序不同。

两者的区别:

1、同步栅栏:将栅栏任务插入队列时,需等待栅栏任务结束后,才会继续插入并执行被写在它后面的任务;

2、异步栅栏:将栅栏任务插入队列后,不会等待栅栏任务结束,而是继续把后续任务插入队列中,栅栏任务结束后才执行后面的任务;

3、栅栏不论异步还是同步,都只会给自己并发队列中的任务设置栅栏,不会阻碍主线程的代码。

5.延时:dispatch_after

1
2
3
4
5
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
//2秒后的延时任务 do something
});

6.dispatch_once

作用:广泛使用在单例中,用以保证初始化方法中的任务只执行一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static ASDFSingleton *mSingleton = nil;

// 声明单例的方式1:
+ (instancetype)shareInstance
{
return [[self alloc] init];
}

//防止外部通过alloc或new的方式错误的创建了实例对象
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mSingleton = [super allocWithZone:zone];
});
return mSingleton;
}

7.dispatch_apply

作用:按指定的次数将任务追加到指定的队列中,并等到全部的处理执行结束。

#示例7.1:

1
2
3
4
5
6
7
8
9
10
11
// 并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("Name",DISPATCH_QUEUE_CONCURRENT);
NSArray *array = @[@"a", @"b", @"c", @"d", @"e", @"f", @"g", @"h", @"i", @"j"];

NSLog(@"++++开始");

dispatch_apply([array count], concurrentQueue, ^(size_t index) {
NSLog(@"++%zu,%@,ON:%@", index, [array objectAtIndex:index], [NSThread currentThread]);
});

NSLog(@"++++结束");

输出日志:

1
2
3
4
5
6
7
8
9
10
11
12
++++开始
++1,b,ON:<NSThread: 0x6000010407c0>{number = 4, name = (null)}
++0,a,ON:<NSThread: 0x60000100d980>{number = 1, name = main}
++2,c,ON:<NSThread: 0x6000010407c0>{number = 4, name = (null)}
++3,d,ON:<NSThread: 0x60000100d980>{number = 1, name = main}
++4,e,ON:<NSThread: 0x600001055700>{number = 5, name = (null)}
++5,f,ON:<NSThread: 0x6000010407c0>{number = 4, name = (null)}
++7,h,ON:<NSThread: 0x600001055700>{number = 5, name = (null)}
++6,g,ON:<NSThread: 0x60000100d980>{number = 1, name = main}
++8,i,ON:<NSThread: 0x6000010407c0>{number = 4, name = (null)}
++9,j,ON:<NSThread: 0x60000100d980>{number = 1, name = main}
++++结束

多运行几次,可以从日志中发现:

  • 打印的索引值并非完全按照 0~9 的升序排列;
  • 遍历元素的操作并非都在同一个线程中;

如果希望索引值完全按照 0~9 的升序排列,可以使用串行队列,这样就类似于for循环了。

#示例7.2:

1
2
3
4
5
6
7
8
9
10
11
// 串行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("Name",DISPATCH_QUEUE_SERIAL);
NSArray *array = @[@"a", @"b", @"c", @"d", @"e", @"f", @"g", @"h", @"i", @"j"];

NSLog(@"++++开始");

dispatch_apply([array count], concurrentQueue, ^(size_t index) {
NSLog(@"++%zu,%@,ON:%@", index, [array objectAtIndex:index], [NSThread currentThread]);
});

NSLog(@"++++结束");

日志:

1
2
3
4
5
6
7
8
9
10
11
12
++++开始
++0,a,ON:<NSThread: 0x600001999600>{number = 1, name = main}
++1,b,ON:<NSThread: 0x600001999600>{number = 1, name = main}
++2,c,ON:<NSThread: 0x600001999600>{number = 1, name = main}
++3,d,ON:<NSThread: 0x600001999600>{number = 1, name = main}
++4,e,ON:<NSThread: 0x600001999600>{number = 1, name = main}
++5,f,ON:<NSThread: 0x600001999600>{number = 1, name = main}
++6,g,ON:<NSThread: 0x600001999600>{number = 1, name = main}
++7,h,ON:<NSThread: 0x600001999600>{number = 1, name = main}
++8,i,ON:<NSThread: 0x600001999600>{number = 1, name = main}
++9,j,ON:<NSThread: 0x600001999600>{number = 1, name = main}
++++结束

不论使用串行还是并发队列,dispatch_apply函数都不会立刻返回,它会阻塞当前线程并在循环完成后再继续执行后续代码。所以可以推测它使用了同步的方式提交遍历元素的任务,这一点类似于for循环。

8.任务组

1.dispatch_group

场景:在并发队列的N个任务执行完毕后,继续执行某任务。

#示例8.1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//自定义的并发队列1
dispatch_queue_t dispatchQueue1 = dispatch_queue_create("concurrentQueue1",DISPATCH_QUEUE_CONCURRENT);

//自定义的并发队列2
dispatch_queue_t dispatchQueue2 = dispatch_queue_create("concurrentQueue2",DISPATCH_QUEUE_CONCURRENT);

dispatch_group_t dispatchGroup = dispatch_group_create();

NSLog(@"++++1:%@",[NSThread currentThread]);

//并发队列1上执行任务1
dispatch_group_async(dispatchGroup, dispatchQueue1, ^(){
for (int i = 0; i < 1000; i++) {
if (888 == i) {
NSLog(@"++++任务1:%@",[NSThread currentThread]);
}
}
});

//并发队列1上执行任务2
dispatch_group_async(dispatchGroup, dispatchQueue1, ^(){
[NSThread sleepForTimeInterval:5];
NSLog(@"++++任务2:%@",[NSThread currentThread]);
});

//并发队列2上执行任务3
dispatch_group_async(dispatchGroup, dispatchQueue2, ^(){
[NSThread sleepForTimeInterval:3];
NSLog(@"++++任务3:%@",[NSThread currentThread]);
});

//任务执行完成
dispatch_group_notify(dispatchGroup,dispatch_get_main_queue(), ^(){
NSLog(@"++++任务完成:%@",[NSThread currentThread]);
});

NSLog(@"++++稍等一哈:%@",[NSThread currentThread]);
dispatch_async(dispatchQueue2, ^{
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);
NSLog(@"++++哈~终于等到你:%@",[NSThread currentThread]);
});

NSLog(@"++++2:%@",[NSThread currentThread]);

日志:

1
2
3
4
5
6
7
8
++++1:<NSThread: 0x60000313a800>{number = 1, name = main}
++++稍等一哈:<NSThread: 0x60000313a800>{number = 1, name = main}
++++任务1:<NSThread: 0x600003164780>{number = 5, name = (null)}
++++2:<NSThread: 0x60000313a800>{number = 1, name = main}
++++任务3:<NSThread: 0x600003153cc0>{number = 6, name = (null)}
++++任务2:<NSThread: 0x600003176480>{number = 3, name = (null)}
++++任务完成:<NSThread: 0x60000313a800>{number = 1, name = main}
++++哈~终于等到你:<NSThread: 0x600003176880>{number = 4, name = (null)}

注意事项:

1、一个Group可以和多个queue关联。

2、如果提交到队列中的与Group关联起来的任务全都执行完毕,则会调用dispatch_group_notify并且dispatch_group_wait会停止等待;

3、假设一个队列中有2个任务,只有第二个与Group进行了关联,则只要第二个任务完成,不论第一个任务是否已完成,都会收到dispatch_group_notify通知。

4、dispatch_group_wait会阻塞当前线程,直到任务全部完成或等待时间超过设置的超时时间,所以不能在主线程调用。

2.dispatch_group_enter

dispatch_group_enter+dispatch_group_leave成对用,作用与dispatch_group_async类似:

#示例8.2.1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//自定义的并发队列
dispatch_queue_t queue = dispatch_queue_create("concurrentQueue1", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_t group = dispatch_group_create();

NSLog(@"++++1");

//并发队列上执行任务1
dispatch_group_enter(group);
dispatch_async(queue, ^{
for (int i = 0; i < 1000; i++) {
if (888 == i) {
NSLog(@"++++任务1");
}
}
dispatch_group_leave(group);
});

//并发队列上执行任务2
dispatch_group_enter(group);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:5];
NSLog(@"++++任务2");
dispatch_group_leave(group);
});

//任务执行完成
dispatch_group_notify(group, dispatch_get_main_queue(), ^(){
NSLog(@"++++任务完成");
});

与 dispatch_group_async 的区别(线程同步问题):

如果 dispatch_group_async 里执行的是异步代码,dispatch_group_notify 会直接触发而不会等待异步任务完成;而 dispatch_group_enter、和 dispatch_group_leave 则不会有这个问题,它们只需要在任务开始前 enter 结束后 leave 即可达到线程同步的效果。

#示例8.2.2:dispatch_group_async 执行异步任务时的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//自定义的并发队列1
dispatch_queue_t dispatchQueue1 = dispatch_queue_create("concurrentQueue1",DISPATCH_QUEUE_CONCURRENT);

//自定义的并发队列2
dispatch_queue_t dispatchQueue2 = dispatch_queue_create("concurrentQueue2",DISPATCH_QUEUE_CONCURRENT);

dispatch_group_t dispatchGroup = dispatch_group_create();

//并发队列1上执行任务1
dispatch_group_async(dispatchGroup, dispatchQueue1, ^(){
dispatch_async(dispatchQueue2, ^{
[NSThread sleepForTimeInterval:5];
NSLog(@"++++任务1");
});
});

//并发队列1上执行任务2
dispatch_group_async(dispatchGroup, dispatchQueue1, ^(){
dispatch_async(dispatchQueue2, ^{
[NSThread sleepForTimeInterval:8];
NSLog(@"++++任务2");
});
});

//任务执行完成
dispatch_group_notify(dispatchGroup,dispatch_get_main_queue(), ^(){
NSLog(@"++++任务完成");
});

输出日志为:

1
2
3
++++任务完成
++++任务1
++++任务2

notify在任务开始时就触发了~

#示例8.2.3:dispatch_group_enterleave 执行异步任务时的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//自定义的并发队列
dispatch_queue_t queue = dispatch_queue_create("concurrentQueue1",DISPATCH_QUEUE_CONCURRENT);

dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:5];
NSLog(@"++++任务1");
dispatch_group_leave(group);
});

//并发队列1上执行任务2
dispatch_group_enter(group);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:8];
NSLog(@"++++任务2");
dispatch_group_leave(group);
});

//任务执行完成
dispatch_group_notify(group,dispatch_get_main_queue(), ^(){
NSLog(@"++++任务完成");
});

日志:

1
2
3
++++任务1
++++任务2
++++任务完成

notify 在任务结束时才触发!

3.其他方案

通过组合GCD已有的其他API,等效实现dispatch_group的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 需要执行的任务总数
NSUInteger totalTasks = 10;
// 当前已经完成的任务数量
volatile NSUInteger completedTasks = 0;
// 最大并发数
NSUInteger maxConcurrentTasks = 3;

// 创建一个串行队列,用于等待所有任务完成后执行接下来的任务
dispatch_queue_t completionQueue = dispatch_queue_create("com.example.completion", DISPATCH_QUEUE_SERIAL);

// 创建一个并发队列,用于执行耗时任务
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrent", DISPATCH_QUEUE_CONCURRENT);

// 定义一个任务完成的回调函数
void (^taskCompletionBlock)(void) = ^{
// 原子操作,累加已完成任务数
NSUInteger count = OSAtomicIncrement64(&completedTasks);
if (count == totalTasks) {
// 所有任务都已完成,执行接下来的任务
dispatch_async(completionQueue, ^{
// 在这里执行接下来的任务
NSLog(@"所有任务都已完成");
});
}
};

// 循环提交所有任务
for (NSUInteger i = 0; i < totalTasks; i++) {
dispatch_async(concurrentQueue, ^{
// 在这里执行耗时任务,比如网络请求
NSLog(@"任务 %lu 开始执行", i);
[NSThread sleepForTimeInterval:1.0]; // 模拟耗时操作
NSLog(@"任务 %lu 完成", i);
taskCompletionBlock(); // 调用任务完成的回调函数
});
if (i % maxConcurrentTasks == 0) {
// 当前并发任务已达到最大并发数,等待所有并发任务完成后再继续提交
dispatch_barrier_sync(concurrentQueue, ^{
// do nothing
});
}
}

9.挂起/恢复:

dispatch_suspend-挂起,dispatch_resume-恢复,可以暂停、恢复队列上的任务。但suspend并不保证能立即停止队列上已经在运行的 block。

#示例9.1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
dispatch_queue_t queue = dispatch_queue_create("Queue", DISPATCH_QUEUE_SERIAL);

//提交第一个任务,延时5秒输出日志
dispatch_async(queue, ^{
sleep(5);
NSLog(@"++++第1个任务 延时5秒咯");
});
//提交第二个任务,延时5秒输出日志
dispatch_async(queue, ^{
sleep(5);
NSLog(@"++++第2个任务 延时5秒咯");
});

//延时1秒
NSLog(@"++++延时1秒");
sleep(1);

//挂起队列
NSLog(@"++++挂起队列");
dispatch_suspend(queue);

//延时10秒
NSLog(@"++++延时10秒");
sleep(10);

//恢复队列
NSLog(@"++++恢复队列");
dispatch_resume(queue);

日志:

1
2
3
4
5
6
12:58:11.187713 ++++延时1
12:58:12.189074 ++++挂起队列
12:58:12.189450 ++++延时10
12:58:16.193140 ++++第1个任务 延时5秒咯
12:58:22.190424 ++++恢复队列
12:58:27.192363 ++++第2个任务 延时5秒咯

可以看出,在dispatch_suspend挂起队列后,第一个 block 还是在运行,并且正常输出。

10.dispatch_set_target_queue

1.设置队列优先级

我们自己创建的队列使用的是默认优先级,而系统提供的全局队列则可以指定优先级,通过dispatch_set_target_queue方法我们可以让自定义队列的优先级与全局队列保持一致,从而达到修改自定义队列优先级的目的。

1
2
3
4
5
6
dispatch_queue_t serialQueue = dispatch_queue_create("com.xx.xx",DISPATCH_QUEUE_SERIAL);

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0);

//第一个参数是目标队列,第二个参数是参考队列(全局队列);
dispatch_set_target_queue(serialQueue, globalQueue);

2.任务调度

修改队列中任务的目标队列,把需要执行的任务对象指定到目标队列中去处理。

#示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)seriaQues{
//三个串行队列
dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("queue3", DISPATCH_QUEUE_SERIAL);

dispatch_async(queue1, ^{
NSLog(@"++++任务1开始");
[NSThread sleepForTimeInterval:3.f];
NSLog(@"++++任务1结束");
});
dispatch_async(queue2, ^{
NSLog(@"++++任务2开始");
[NSThread sleepForTimeInterval:2.f];
NSLog(@"++++任务2结束");
});
dispatch_async(queue3, ^{
NSLog(@"++++任务3开始");
[NSThread sleepForTimeInterval:1.f];
NSLog(@"++++任务3结束");
});
}

日志:

1
2
3
4
5
6
++++任务2开始
++++任务1开始
++++任务3开始
++++任务3结束
++++任务2结束
++++任务1结束

多个串行队列异步执行时,block中各任务是并发执行的,它们之间开始和结束的顺序是不确定的。如果想让这些任务按照先后顺序一个个执行,则可以使用dispatch_set_target_queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (void)seriaMultQue{
//三个串行队列
dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("queue3", DISPATCH_QUEUE_SERIAL);

//目标队列
dispatch_queue_t targetQueue = dispatch_queue_create("targetQueue", DISPATCH_QUEUE_SERIAL);

//将三个串行队列合并到目标串行队列中
dispatch_set_target_queue(queue1, targetQueue);
dispatch_set_target_queue(queue2, targetQueue);
dispatch_set_target_queue(queue3, targetQueue);

dispatch_async(queue1, ^{
NSLog(@"++++任务1开始");
[NSThread sleepForTimeInterval:3.f];
NSLog(@"++++任务1结束");
});
dispatch_async(queue2, ^{
NSLog(@"++++任务2开始");
[NSThread sleepForTimeInterval:2.f];
NSLog(@"++++任务2结束");
});
dispatch_async(queue3, ^{
NSLog(@"++++任务3开始");
[NSThread sleepForTimeInterval:1.f];
NSLog(@"++++任务3结束");
});
}

日志:

1
2
3
4
5
6
++++任务1开始
++++任务1结束
++++任务2开始
++++任务2结束
++++任务3开始
++++任务3结束

原先相互之间并发执行的三个任务,现在有序执行了~

另外,执行这个操作后,queue1上已在执行的任务会继续在queue1执行,尚未执行的任务会在targetQueue上执行。

11.设置队列标志

作用:dispatch_queue_set_specific用来向指定队列里面设置一个标识,配合dispatch_get_specific使用。

#示例11.1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
static const void *key = "queueKey";
void *context = "myQueueContext";

dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_set_specific(queue1, key,context, NULL);

if (dispatch_get_specific(key)) {
//当前队列是主队列,不是queue1,所以取不到key对应的值。
NSLog(@"++++主队列++queue1?:YES");
}else{
NSLog(@"++++主队列++queue1?:NO");
}

dispatch_async(queue1, ^{
if (dispatch_get_specific(key)) {
//当前队列是queue1,所以能取到specificKey对应的值。
NSLog(@"++++异步串行队列++queue1?:YES");
}else{
NSLog(@"++++异步串行队列++queue1?:NO");
}
});

return YES;
}

日志:

1
2
++++主队列++queue1?:NO
++++异步串行队列++queue1?:YES

12.dispatch_block_cancel

作用:取消单个任务。

这个任务必须是用dispatch_block_create创建dispatch_block_t。再者,此方法只对尚未执行的block有效,对正在执行中的任务无效。

#示例12.1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//取消任务3
dispatch_queue_t conQueue = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_block_t block1 = dispatch_block_create(0, ^{
sleep(5);
NSLog(@"++++block1");
});

dispatch_block_t block2 = dispatch_block_create(0, ^{
NSLog(@"++++block2");
});

dispatch_block_t block3 = dispatch_block_create(0, ^{
NSLog(@"++++block3");
});

dispatch_async(conQueue, block1);
dispatch_async(conQueue, block2);
dispatch_async(conQueue, block3);
dispatch_block_cancel(block3);

日志:

1
2
++++block2
++++block1

13.dispatch_semaphore

信号量:就是一种可用来控制访问资源的数量的标识。当一个线程在进入一段关键代码之前,线程须获取一个信号量,一旦该关键代码段执行完成,那么该线程须释放其持有的信号量。其它想进入该关键代码段的线程须等待可用信号量,如果没有可用信号量,则需等待前面的线程释放信号量。

有一个经典的停车场的例子:信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait就相当于来了一辆车,dispatch_semaphore_signal就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(value:Int))),调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait剩余车位就减少一个;当剩余车位为0时,再来车(即调用dispatch_semaphore_wait)就只能等待。有耐心的车主会一直等下去,没耐心的车主在等待“一段时间”之后就会离开。

1
2
3
4
5
6
7
8
//创建信号量,参数:信号量的初值,如果小于0则会返回NULL
dispatch_semaphore_create(信号量值)

//减少信号量,时间:DISPATCH_TIME_NOW、DISPATCH_TIME_FOREVER
dispatch_semaphore_wait(信号量,等待时间)

//释放信号量
dispatch_semaphore_signal(信号量)

#示例13.1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)semaphore{
dispatch_queue_t mQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

NSLog(@"+++1");
dispatch_async(mQueue, ^{
sleep(2);
NSLog(@"+++first task");
dispatch_semaphore_signal(semaphore);
});
NSLog(@"+++2");
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

NSLog(@"+++3");
dispatch_async(mQueue, ^{
sleep(2);
NSLog(@"+++second task");
dispatch_semaphore_signal(semaphore);
});
NSLog(@"+++4");
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

NSLog(@"+++5");
}

输出日志:

1
2
3
4
5
6
7
13:18:59 +++1
13:18:59 +++2
13:19:01 +++first task
13:19:01 +++3
13:19:01 +++4
13:19:03 +++second task
13:19:03 +++5

示例中创建了初始值=0的信号量,随后开始了两个异步打印数字的任务。从日志来看,“+++first task” 在”+++3” 之前,而“+++5”是最后才打印的,这是因为信号量=0时dispatch_semaphore_wait()处自动阻塞了当前线程,两个打印任务执行完之后dispatch_semaphore_signal()使信号量=1,当前线程才得以继续执行。

#示例13.2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//初始信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_queue_t quene = dispatch_queue_create("com.M.D", DISPATCH_QUEUE_CONCURRENT);

//任务1
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"+++任务1");
sleep(1);
NSLog(@"+++完成任务1");
dispatch_semaphore_signal(semaphore);
});

//任务2
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"+++任务2");
sleep(1);
NSLog(@"+++完成任务2");
dispatch_semaphore_signal(semaphore);
});

//任务3
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"+++任务3");
sleep(1);
NSLog(@"+++完成任务3");
dispatch_semaphore_signal(semaphore);
});

输出日志:

1
2
3
4
5
6
+++任务1
+++完成任务1
+++任务2
+++完成任务2
+++任务3
+++完成任务3

ps,信号量的使用只保证了并发场景下每次只有一个线程执行任务,并不能保证任务的执行顺序,再运行一次后日志如下:

1
2
3
4
5
6
+++任务1
+++完成任务1
+++任务3
+++完成任务3
+++任务2
+++完成任务2

14.GCD倒计时

#示例14.1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//@property (strong, nonatomic) dispatch_source_t aTimer;

- (void) GCDTimer
{
__block int timeout = 60; //倒计时时间
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

_aTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,queue);

dispatch_source_set_timer(_aTimer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0); //每秒执行

dispatch_source_set_event_handler(_aTimer, ^{
if(timeout<=0){ //倒计时结束,关闭
dispatch_source_cancel(_aTimer);
dispatch_async(dispatch_get_main_queue(), ^{
//设置界面的按钮显示 根据自己需求设置
});
}
else{
dispatch_async(dispatch_get_main_queue(), ^{
//设置界面的按钮显示 根据自己需求设置
});
timeout--;
}
});
dispatch_resume(_aTimer);
}

GCD定时器并不是由NSTimer或者CFRunLoopTimer实现的,无需加入到RunloopMode中,所以不会出现因RunloopMode切换而失效的问题。

15.GCD读写锁

为了保证数据访问或者方法调用时的线程安全,一般我们可以使用来实现同步机制,比如@synchronized()NSLock等。但是,频繁的上锁、释放锁的操作会降低执行效率。所以我们可以考虑使用dispatch_barrier_async,通过 GCD 对任务的调度来达到相同的效果。

思路:dispatch_barrier_async能确保在其之前提交到队列的任务先执行,然后执行它自己提交的任务,最后执行其后提交的任务。利用这个特点,我们可以将数据的读写操作放入一个并发队列中,写数据的任务通过栅栏提交到队列中,读数据的任务则可以同步的提交到队列中。这样就能保证执行写操作时,读操作或其他写操作都被阻塞,而执行读操作时,写操作被阻塞,将读写分开从而避免数据的污染。

#示例15.1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@interface AppDelegate()
@property(nonatomic, strong) NSMutableArray *mArr;
@property (nonatomic, strong) dispatch_queue_t mPropertyConcurrentQueue;
@end

@implementation AppDelegate

@synthesize mArr = _mArr;

- (NSMutableArray *)mArr{
__block NSMutableArray *tmpArr;
dispatch_sync(_mPropertyConcurrentQueue, ^{//并发队列+同步=串行,不能使用dispatch_async,会直接返回
tmpArr = _mArr;
});
return tmpArr;
}

- (void)setMArr:(NSMutableArray *)mArr{
dispatch_barrier_async(_mPropertyConcurrentQueue, ^{
_mArr = mArr;
});
}

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//使用自己创建的并发队列,不能使用系统的dispatch_get_global_queue,上面章节中有讲到这一点
_mPropertyConcurrentQueue = dispatch_queue_create("readWriteQueue", DISPATCH_QUEUE_CONCURRENT);

for (int i = 0; i < 10; i++) {
dispatch_queue_t tmpQueue = dispatch_queue_create("1", DISPATCH_QUEUE_CONCURRENT);
if ((i % 2) == 0) {
dispatch_async(tmpQueue, ^{
NSLog(@"+++Reset array,thread:%@",[NSThread currentThread]);
self.mArr = [NSMutableArray array];
});
}else{
dispatch_async(tmpQueue, ^{
NSLog(@"++++Add num:%d,thread:%@",i,[NSThread currentThread]);
[self.mArr addObject:@(i)];
});
}
}

return YES;
}
@end

GCD
https://davidlii.cn/2017/11/08/gcd.html
作者
Davidli
发布于
2017年11月8日
许可协议