gcd 全面解析

GCD 概要

Grand Central Dispatch

GCD 是什么?

是异步执行任务的技术之一。通过非常简洁的方式,实现了极为复杂的多线程编程。说白了就是为了使得多线程编程更加简洁

Objective-C

1
2
3
4
5
6
7
8
9
10
11
12
- (void)gcdTest {

dispatch_queue_t queue = dispatch_queue_create("com.hello.world", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
//处理耗时操作
NSLog(@"处理耗时操作 ...");
sleep(3);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"refresh ui ...");
});
});
}

Swift

1
2
3
4
5
6
7
8
9
10
11
12
func gcdTestFunction() {
let queue = DispatchQueue(label: "queue0", qos: .default, attributes: .concurrent, autoreleaseFrequency: .never, target: nil)
queue.async {
/*长时间处理任务*/
print("处理耗时操作")
sleep(3)
DispatchQueue.main.async {
//主线程处理,比如用户界面更新
print("refresh ui ... ")
}
}
}

多线程编程

一个 CPU 无分叉的执行一条条的指令路径,即为线程。
一个 CPU 在各条指令路径之间进行切换,即为多线程,而如果一个 CPU 有多个核,那么就是真正的同时执行多条指令路径,这就是多线程编程技术。

缺点:

  1. 数据不一致(数据共享造成)
  2. 消耗资源(寄存器保存上下文,供 CPU 在各个指令路径之间进行切换)
  3. 死锁(多个线程相互等待)

鉴于上述问题,多线程编程技术就会趋于复杂。

优点:
多线程编程可保证应用程序的响应性能(耗时操作放在子线程中执行,主线程负责 UI 响应)

通过多线程技术,在执行长时间的处理时仍可保证用户界面的响应性能。
GCD 大大简化了偏于复杂的多线程编程源代码。

API

使用 GCD 开发者要做的就是定义要执行的任务然后添加到适当的 Dispatch Queue 中。
GCD 为我们提供了 API 供完成队列的创建和任务的添加。

Dispatch Queue

Dispatch Queue 是执行任务的队列。将任务添加到队列中,任务就会被执行。队列分为两类:

  • 串行队列:等在当前队列中的任务执行完毕之后,在执行下一个任务
  • 并发队列:不会等待当前的任务执行完毕,根据系统情况继续执行后面追加的任务。

Main Dispatch Queue/ Global Dispatch Queue


创建队列的方式,除了通过 dispatch_create() 之外,系统 API 帮我提供了两个可以直接获取到队列的函数,分别是:

  • dispatch_get_main_queue() 获取主队列,向其中添加的任务在主线程的 Runloop 中执行,因此只有一个这种队列,是串行队列
  • dispatch_get_global_queue() 获取全局队列,是并发队列

dispatch_set_target_queue

修改队列的优先级,因为通过 create 函数创建的队列没有优先级,那么可以通过这个函数来设置优先级。

dispatch_after

这个函数是在指定时间后像当前队列中追加任务,至于任务合适执行要看当前的上下文了。

dispatch_group

如果一串任务有序列执行,可以直接通过串行队列实现,但是如果多个任务无序执行结束之后,又有一个确定的任务必须等待前面的无序任务执行结束之后再去执行,那么这就需要用到这个函数了: 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
- (void)dispatch_group {

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(@"one ...");
});

dispatch_group_async(group, queue, ^{
NSLog(@"two ...");
});

dispatch_group_async(group, queue, ^{
NSLog(@"three");
});


dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"done");
});


}

one、two、three 三个任务追加到并发队列中,虽然这个三个任务执行顺序不定,但是 done 一定是在最后执行的。

dispatch_barrier_async

执行读写任务,读任务是可以并发的,但是写任务相互之间不可以并发,与读任务可以控制着进行并发。
下面共有 8 个读取任务,执行完成四个读取任务之后,需要进行一次写任务,这个写入任务必须在前四个读取任务结束之后再执行,那么久可以通过 dispatch_barrier_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
29
30
31
32
- (void)dispatch_barrier_async {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(queue, ^{
NSLog(@"reading ..... 1");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 2");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 3");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 4");
});

dispatch_barrier_async(queue, ^{
NSLog(@"writing ...");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 5");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 6");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 7");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 8");
});
}

使用 Concurrent Dispatch Queue 和 dispatch_barrier_async 函数可实现高效率的数据库访问和文件访问

dispatch_sync

同步追加任务到某个队列中。同步追加方式要等到之前的任务完成之后,才会追加进去,所以容易造成死锁:

下面这两种方式就会造成死锁:

1
2
3
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"one ...");
});
1
2
3
4
5
6
7
dispatch_queue_t queue = dispatch_queue_create("one", NULL);
dispatch_async(queue, ^{
NSLog(@"one ...");
dispatch_sync(queue, ^{
NSLog(@"Hello world");
});
});

dispatch_apply

1
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t dispatch_global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, dispatch_global_queue, ^(size_t t) {
NSLog(@"Hello world %zu", t);
});
NSLog(@"done --- ");

dispatch_queue_t dispatch_serial_queue = dispatch_queue_create("com.maple.www", NULL);
dispatch_apply(10, dispatch_serial_queue, ^(size_t t) {
NSLog(@"Hello world serial %zu", t);
});
NSLog(@"done");

这个函数像是 dispatch_sync 和 dispatch group 的组合版。该函数按照指定次将指定的 Block 追加都队列中,并且等待所有的任务执行完毕。

可以通过这个函数来访问数组元素:

1
2
3
4
5
NSArray *array = @[@"one",@"two",@"three",@"four",@"five",@"six"];

dispatch_apply([array count], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {
NSLog(@"array[%zu] = %@",index,array[index]);
});

因为 dispatch_apply 函数和dispatch_sync 函数相同,所以最好在 dispatch_async 函数中非同步执行 dispatch_apply 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

NSArray *array = @[@"one",@"two",@"three",@"four",@"five",@"six"];
dispatch_queue_t dispatch_global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(dispatch_global_queue, ^{

dispatch_apply([array count], dispatch_global_queue, ^(size_t index) {
NSLog(@"array[%zu] = %@",index,array[index]);
});

// 到这里已经处理完所有的事情

dispatch_async(dispatch_get_main_queue(), ^{
//更新ui
});
});

dispatch_suspend / dispatch_resume

挂起和恢复队列

dispatch semaphore

信号基数,如果我们直接通过 for 循环启动多个异步线程向数组中存入数据,那么会引发内存分配的错误。而 semaphore 是一种信号机制,当我们通过 create 函数创建一个 count 为 1 的信号时候,表示当前只能执行一次任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)semaphore {

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

NSMutableArray *mArray = [[NSMutableArray alloc] init];

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

for (int i = 0; i < 100000; i ++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[mArray addObject:[NSNumber numberWithInt:i]];
dispatch_semaphore_signal(semaphore);
});
}

NSLog(@"over");
}

dipatch_once

在当前应用程序中如果打算只执行一次任务,那么就可以通过 dispatch_once 来实现。一般用来生成单例对象。

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
48
49
50
51
52
53
54
- (void)dispatch_onceFunc {

static dispatch_once_t onceToken;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_once(&onceToken, ^{
NSLog(@"Hello world");
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_once(&onceToken, ^{
NSLog(@"Hello world");
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_once(&onceToken, ^{
NSLog(@"Hello world");
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_once(&onceToken, ^{
NSLog(@"Hello world");
});
});


static int initialized = NO;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (initialized == NO) {
NSLog(@"Hello this is mine.");
initialized = YES;
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (initialized == NO) {
NSLog(@"Hello this is mine.");
initialized = YES;
}
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (initialized == NO) {
NSLog(@"Hello this is mine.");
initialized = YES;
}
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (initialized == NO) {
NSLog(@"Hello this is mine.");
initialized = YES;
}
});

}

output:

1
2
3
4
5
GCDExample[79831:10334594] Hello world
GCDExample[79831:10334575] Hello this is mine.
GCDExample[79831:10334638] Hello this is mine.
GCDExample[79831:10334639] Hello this is mine.
GCDExample[79831:10334640] Hello this is mine.

使用 dispatch_once 保证了线程安全。

Dispatch I/O

当我们读取较大的文件的时候,如果将文件分成合适大小并使用 Global Dispatch Queue 并列读取的话,应该比一般的读取速度会快很多。

实现

GCD 的 Dispatch Queue 非常方便,那么它究竟是怎么实现的 ?

  • 用户管理追加 Block 的 C 语言层实现 FIFO 队列
  • Atomic 函数中实现的用于排他控制的轻量级信号
  • 用于管理线程的 C 语言层实现的一些容器