iOS GCD详细介绍

简介

GCD(Grand Central Dispatch)是在macOS10.6提出来的,后来在iOS4.0被引入。GCD的引入主要是它的使用比传统的多线程方案如NSThreadNSOperationQueueNSInvocationOperation使用起来更加方便,并且GCD的运作是在系统级实现的。由于是作为系统的一部分来实现的,因此比以前的线程更加有效。
同时GCD使用了block语法,在书写上变得更加简洁。

至于什么是多线程,多线程编程的优缺点这里就不探讨了,主要讨论一下GCD的使用。

Dispatch Queue介绍

关于GCD,苹果所给出最直接的描述是:将想要执行的任务添加到Dispatch Queue中。因此Dispatch Queue将是接下来讨论的关键。
先来看下面这段代码:

1
2
3
dispatch_async(queue, ^{
// 要执行的任务
});

dispatch_async()是向队列中添加任务的函数。这段代码是将要执行的任务以block代码块的形式作为参数,添加到queue的队列中,而queue则会按照顺序处理队列中的任务

另外,Dispatch Queue以处理方式的不同,分为两种:

  • Serial Dispatch Queue,顺序依次执行,只有队列中前一个任务执行完成,后一个才可以开始。也就是我们常说的串行队列
  • Concurrent Dispatch Queue,并发执行,将队列中的任务依次添加到并行的线程中,同时执行。也就是我们常说的并行队列。⚠️注意:能够同时执行任务的个数取决于系统当前的处理能力

⚠️注意:Dispatch Queue队列并不是指我们印象中的线程,它是任务队列,它只负责任务的管理调度,并不进行任务的执行操作,任务的执行是由Dispatch Queue分配的线程来完成的

Dispatch Queue创建

在了解了什么是Dispatch Queue后,来看一下Dispatch Queue是如何得到的,先来看一段代码:

1
2
dispatch_queue_t aSerialDispatchQueue =
dispatch_queue_create("MySerialDispatchQueue", NULL);

这段代码就是通过dispatch_queue_create()函数得到一个Dispatch Queue

其中,第一个参数是指Dispatch Queue的名称,可以设置为NULL但是不建议这样做,因为在XcodeInstruments调试的时候都会以设置的这个参数作为展示名称,所以建议创建的每一个Dispatch Queue都设置一个合适的名称;

函数的第二个参数设置成了NULL,此时得到的是Serial Dispatch Queue类型的队列,也可以直接设置第二个参数为DISPATCH_QUEUE_SERIAL,就像这样:

1
2
dispatch_queue_t aSerialDispatchQueue =
dispatch_queue_create("MySerialDispatchQueue", DISPATCH_QUEUE_SERIAL);

如果我们想得到一个Concurrent Dispatch Queue类型的队列,第二个参数要设置为DISPATCH_QUEUE_CONCURRENT,就像这样:

1
2
dispatch_queue_t aConcurrentDispatchQueue =
dispatch_queue_create("MyConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

得到的返回值的类型都为dispatch_queue_t

####验证
下面用代码来验证一下这两种队列是否像上边说的那样执行的。
先来验证一下Serial Dispatch Queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatch_queue_t serialQueue 
= dispatch_queue_create("queue_1", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
NSLog(@"任务1 begin");
[NSThread sleepForTimeInterval:3.f];
NSLog(@"任务1 stop");
});
dispatch_async(serialQueue, ^{
NSLog(@"任务2 begin");
[NSThread sleepForTimeInterval:2.f];
NSLog(@"任务2 stop");
});
dispatch_async(serialQueue, ^{
NSLog(@"任务3 begin");
[NSThread sleepForTimeInterval:1.f];
NSLog(@"任务3 stop");
});

看一下打印结果:

再来验证一下Concurrent Dispatch Queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dispatch_queue_t concurrentQueue 
= dispatch_queue_create("queue_2", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(concurrentQueue, ^{
NSLog(@"任务1 begin");
[NSThread sleepForTimeInterval:3.f];
NSLog(@"任务1 stop");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"任务2 begin");
[NSThread sleepForTimeInterval:2.f];
NSLog(@"任务2 stop");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"任务3 begin");
[NSThread sleepForTimeInterval:1.f];
NSLog(@"任务3 stop");
});

看一下打印结果:

多个Dispatch Queue之间的关系

通过上面的验证确实可以看出Serial Dispatch Queue是串行执行、Concurrent Dispatch Queue是并行执行的。那如果我们创建多个Serial Dispatch Queue会怎样呢,这些Serial Dispatch Queue也会按照顺序依次执行么?不是的,它们之间是并发执行的,也就是说多个Dispatch Queue之间是并发执行

那如果想让多个Serial Dispatch Queue依然保持串行执行怎么办呢?后边会继续说。

Dispatch Queue持有与释放

macOS10.8iOS6.0以后,GCD已经支持ARC模式了,所以无需手动管理Dispatch Queue的持有与释放。
这里提一下MRC模式下管理Dispatch Queue的两个函数:

1
2
dispatch_retain(aSerialDispatchQueue);
dispatch_release(aSerialDispatchQueue);

系统提供的Dispatch Queue

除了我们手动创建的Dispatch Queue以外,系统还给我们提供了几个现成的队列,Main Dispatch QueueGlobal Dispatch Queue

  • Main Dispatch Queue是在主线程中执行的Dispatch Queue。因为主线程只有一条,并且主线程中的任务是依次执行的,所以Main Dispatch Queue自然是Serial Dispatch Queue类型的队列,追加到Main Dispatch Queue的任务都是在主线程RunLoop中执行的,像界面更新等一些任务也都是在这个线程中执行。
    获得方法:

    1
    dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
  • Global Dispatch Queue是所有应用程序都能使用的Concurrent Dispatch Queue类型队列。Global Dispatch Queue有四个优先级分别是:高优先级(high priority)默认优先级(default priority)低优先级(low priority)后台优先级(background priority)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 高优先级
    dispatch_queue_t globalDispatchQueueHigh
    = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    // 默认优先级
    dispatch_queue_t globalDispatchQueueDefault
    = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 低优先级
    dispatch_queue_t globalDispatchQueueLow
    = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    // 后台优先级
    dispatch_queue_t globalDispatchQueueBackground
    = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

对于Main Dispatch QueueGlobal Dispatch Queue,即使在MRC模式下,也无需考虑持有与释放问题。即使执行dispatch_retain()dispatch_release()函数也是不会发生任何变化的。

Dispatch Queue目标队列

GCD中的dispatch_set_target_queue()函数可以将一个dispatch_object_t对象设置到目标队列来处理,上边说到的dispatch_queue_t都属于dispatch_object_t对象。

上边曾说过多个Serial Dispatch Queue之间是并行执行的,先来验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dispatch_queue_t queue1 
= dispatch_queue_create("queue_1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2
= dispatch_queue_create("queue_2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3
= dispatch_queue_create("queue_3", DISPATCH_QUEUE_SERIAL);

dispatch_async(queue1, ^{
NSLog(@"任务1 begin");
[NSThread sleepForTimeInterval:3.f];
NSLog(@"任务1 stop");
});
dispatch_async(queue2, ^{
NSLog(@"任务2 begin");
[NSThread sleepForTimeInterval:2.f];
NSLog(@"任务2 stop");
});
dispatch_async(queue3, ^{
NSLog(@"任务3 begin");
[NSThread sleepForTimeInterval:1.f];
NSLog(@"任务3 stop");
});

打印结果:

通过打印结果来看,虽然创建的是3个串行Dispatch Queue,但是串行的Dispatch Queue间却是并行执行关系。
如果我们将创建好的这3个Serial Dispatch 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
dispatch_queue_t targetQueue 
= dispatch_queue_create("target_queue", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t queue1 = dispatch_queue_create("queue_1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("queue_2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("queue_3", 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 begin");
[NSThread sleepForTimeInterval:3.f];
NSLog(@"任务1 stop");
});
dispatch_async(queue2, ^{
NSLog(@"任务2 begin");
[NSThread sleepForTimeInterval:2.f];
NSLog(@"任务2 stop");
});
dispatch_async(queue3, ^{
NSLog(@"任务3 begin");
[NSThread sleepForTimeInterval:1.f];
NSLog(@"任务3 stop");
});

打印结果:

通过打印可以看出,被添加到目标队列里的3个队列,按照串行顺序执行。其实是串行执行还是并行执行跟目标队列的性质有关。

如果将targetQueue换成一个并行队列,相信被执行的3个队列必然是并行执行关系,我已经做了验证:

继续,现在我们将目标队列targetQueue换回成Serial Dispatch Queue串行队列,而将3个被添加的队列换成Concurrent Dispatch Queue并行队列,并分别向其中额外再添加2个任务,此时3个被添加队列中分别包含的任务是:1-11-21-32-12-22-33-13-23-3,再来看一下打印结果:

通过打印结果发现,所有任务都是按照串行顺序执行下来的,被添加的三个并行队列本身的并行特性被失效了。

还没有完,如果把目标队列换成并行的Concurrent Dispatch Queue又会怎样呢?

通过打印结果可以看出,所有的任务都被并行执行。

通过上面的测试可以看出:无论被添加的是什么、什么队列,它们所包含的任务(当然这些任务都是没有被原所在队列执行的)最终都会按照目标队列的自身性质来执行,它们的优先级也遵循目标队列的优先级。

利用dispatch_queue_create()函数生成的Dispatch Queue不管是Serial Dispatch Queue还是Concurrent Dispatch Queue所使用的都是与Global Dispatch Queue默认优先级相同优先级的线程,利用dispatch_set_target_queue()函数我们可以改变它们的优先级。

贴一下官方文档(翻译不好,只能靠你的英文功力了):

延迟追加任务

1
2
3
4
5
dispatch_queue_t mainDispatchQueue =  dispatch_get_main_queue();
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3* NSEC_PER_SEC);
dispatch_after(time,mainDispatchQueue,^{
// 任务...
});

以上代码段是将任务延迟3秒添加到队列中,注意的是:是添加到队列中而不是执行
第一个参数timedispatch_time_t类型,该类型值可通过dispatch_time()函数或dispatch_walltime()函数得到。
dispatch_time()函数的含义是获得从第一个参数指定的时间开始,经过第二个参数指定的时间长度后的时间。DISPATCH_TIME_NOW表示现在的时间,类型为dispatch_time_tNSEC_PER_SEC为秒的单位,NSEC_PER_MSEC为毫秒单位。dispatch_walltime()函数用于计算绝对时间。

Dispatch Group

在实际应用中,经常需要在执行完一些任务后,再执行某一个特定任务。如果使用的是Serial Dispatch Queue只需将任务全部添加到队列中,然后再在最后追加上想要执行的任务就可以了。但是在使用Concurrent Dispatch Queue类型的队列或者同时使用多个Dispatch Queue的时候,想实现这样的需求就比较困难了。
这时,就要用到Dispatch Group了。下面通过代码来看一下Dispatch Group是如何使用的:

1
2
3
4
5
6
7
8
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group,queue, ^{NSLog(@"任务1");});
dispatch_group_async(group,queue, ^{NSLog(@"任务2");});
dispatch_group_async(group,queue, ^{NSLog(@"任务3");});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"最后要执行的任务");})

  • group是通过dispatch_group_create()函数创建的,类型为dispatch_group_t
  • dispatch_group_async()函数与dispatch_async()函数相同,都是向队列中追加任务,不同的是dispatch_group_async()函数中第一个参数是指定当前任务属于哪个Dispatch Group
  • dispatch_group_notify()函数中第一个参数是指定要监视的Dispatch Group,在属于该group的所有任务都执行完成后会将函数的第三个参数任务追加到第二个参数队列中执行。

除了添加对任务的监控以外,还可使用等待函数,来看下面一段代码:

1
2
3
4
5
6
7
8
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue,^{NSLog(@"任务1");});
dispatch_group_async(group, queue,^{NSLog(@"任务2");});
dispatch_group_async(group, queue,^{NSLog(@"任务3");});

dispatch_group_wait(group, 10*NSEC_PER_SEC);

上面代码中的dispatch_group_wait()函数是对所属group的任务的处理过程进行等待,函数中第二个参数代表等待时间,为dispatch_time_t类型。如果在设置时间内所有任务执行完成函数返回long类型的值0,如果返回值不为0说明还有任务在执行中。如果第二个参数设置为DISPATCH_TIME_FOREVER,函数必将返回0,因为该函数将无限期挂起等待,直到所有任务执行完成函数才会返回。
那么等待到底意味着什么?这意味着一旦调用dispatch_group_wait()函数,该函数就处于调用状态而不返回,即执行dispatch_group_wait()函数的所在线程停止。当该函数返回值后,当前线程继续。
如果将函数中第二个参数设置为DISPATCH_TIME_NOW,则不需要等待即可判定所属group的任务是否全部执行完成。

dispatch_barrier_async()函数

通常在进行数据读、写操作的时候,多个任务同时执行读操作是可以的,但是多个任务同时执行写操作可能就会发生数据竞争的问题。尤其在一系列复杂的读写操作中,使用Serial Dispatch Queue会导致读操作效率变低,使用Concurrent Dispatch Queue不但会引起多个写任务发生数据竞争,还可能因为并发执行导致读写顺序错乱。
因此要使用dispatch_barrier_async()函数配合Concurrent Dispatch Queue并行队列来解决这个问题。
来看下面一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
dispatch_queue_t  queue = dispatch_create_queue("OneConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, block_mission1_reading);
dispatch_async(queue, block_mission2_reading);
dispatch_async(queue, block_mission3_reading);
dispatch_async(queue, block_mission4_reading);

dispatch_barrier_async(queue, block_mission5_writing);

dispatch_async(queue, block_mission6_reading);
dispatch_async(queue, block_mission7_reading);
dispatch_async(queue, block_mission8_reading);

dispatch_barrier_async()函数会等到
block_mission1_reading
block_mission2_reading
block_mission3_reading
block_mission4_reading这些任务并行执行完毕后再将block_mission5_writing任务追加到队列中,当dispatch_barrier_async()函数追加的任务执行完成,队列会恢复为一般动作,继续并行处理后续追加到队列中的任务。

Dispatch Queue挂起与恢复

当我们想挂起某一个Dispatch Queue

1
dispatch_suspend(queue);

恢复

1
dispatch_resume(queue);

Dispatch Queue挂起后,追加到队列中但还没有执行的任务在这之后停止执行,恢复后这些任务继续执行。

指定任务只执行一次

通过dispatch_once()函数指定的任务只执行一次,像单例的初始化就可以用该函数来实现。
正常我们书写单例的方法是:

1
2
3
4
5
6
static NSObject obj = nil;
@synchronized (self) {
if (obj == nil) {
obj = ...
}
}

使用dispatch_once()函数的实现方式是:

1
2
3
4
5
static NSObject obj = nil;
static dispatch_once_t pred;
dispatch_once( &pred, ^{
obj = ...
});

使用dispatch_once()函数可以保证在多线程环境下百分之百安全。

dispatch_sync()与dispatch_async()的区别

前面使用频率特别高的添加任务函数dispatch_async(),该函数是非同步的,它只负责将任务添加到队列中,并不在乎添加到队列中的任务是否处理完成,立刻返回。
而相对于dispatch_async()函数的dispatch_sync()函数是同步的,dispatch_sync()函数不但负责将任务添加到队列中,还要等待添加的任务执行完成再返回,在此过程中调用dispatch_sync()函数所在的线程被挂起,直到dispatch_sync()函数返回,线程恢复,注意是调用dispatch_sync()函数的线程被挂起

关于dispatch_sync()函数比较重要的一个问题就是死锁,为什么会出现死锁的情况呢?比如说有一个串行队列,并且dispatch_sync()函数的调用也是在该队列中,这样串行队列的线程在调用dispatch_sync()函数的时候被挂起,而线程被挂起之后dispatch_sync()函数添加的任务一直得不到线程的处理,一直不能返回,所以线程将一直处于被挂起的状态。
出现这种状况的核心问题就是(可能有点绕):调用dispatch_sync()函数的线程(注意是线程,而不是队列,并行队列有多个线程可能并不会发生这种状况,除非调用函数的任务和函数追加的任务被分配到并行队列中同一线程中去)和处理函数追加的任务的线程是同一个线程。此时就会发生死锁。
举两个例子体会一下:

1
2
3
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{NSLog(@"任务");});
// 死锁

1
2
3
4
5
dispatch_queue_t queue = dispatch_queue_create("OneSerialDispatchQueue", NULL);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{NSLog(@"任务");});
});
// 死锁

dispatch_apply()函数

该函数作用是一次性向队列中添加多个任务,并且跟dispatch_sync()函数的使用方式一致,是同步的,只有向队列中添加的所有任务都执行完成才返回。并且dispatch_apply()函数向队列中追加的block任务都是带有参数的,这是为了函数将添加序号作为参数传递给block任务。
看下面一段代码:

1
2
3
4
5
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(8, queue, ^(size_t index){
NSLog(@"这是第%zu任务", index);
});
NSLog(@"所有任务处理完成");

执行结果:

虽然我这里的执行结果是顺序的,但也有可能执行的结果是无序的,因为这里使用的是并行队列。但无论前8个任务的顺序是怎样所有任务处理完成这个任务一定是最后一个执行。
相信理解了同步概念就一定会明白其中原因了。

写了这么多,必然不乏漏洞和错误,欢迎大家指正。

版权声明:出自Jerry LMJ的原创作品,未经作者允许不得转载。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器