第二部分,GCD。

Grand Central Dispatch,它是基于 C 语言的 API,开发者只需要将任务放在 Block 内,并指定好追加的队列,就可以完成多线程开发。

进程和线程?

进程

系统中正在运行的一个程序,进程之间是相互的独立的,每个进程都有属于自己的存储空间。iOS 中的 App,都是独立的进程。

线程

进程内部执行任务所需要的执行路径。进程若想执行任务,则必须得在线程下执行。也就是说,进程至少有一个线程才能执行任务。

如果需要多任务同时进行,就意味着需要多线程的开启和使用。

多线程

实现原理:虽然在同一时刻,CPU 只能处理 1 条线程,但是 CPU 可以快速地在多条线程之间调度(切换),造成了多线程并发执行的假象。

多线程优点

  1. 能适当提高程序的执行效率
  2. 能适当提高资源利用率(CPU、内存)

多线程缺点

  1. 创建线程需要空间和时间成本,iOS 下主要成本包括:在栈空间的子线程 512KB、主线程 1MB,创建线程大约需要 90 毫秒的创建时间。
  2. 线程越多,CPU 在调度线程上的开销就越大
  3. 线程越多,程序设计就越复杂,要考虑线程之间通信,多线程的数据共享等问题

多线程安全

当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。

资源抢夺的解决方案

互斥锁。

当某线程访问一个数据之前就要给数据加锁,让其不被其他的线程所修改。

注意:
这里的线程都是子线程,如果给数据加了锁,就等于将这些异步的子线程变成同步的了,所以也叫做线程同步技术。

互斥锁的使用

1
2
3
@synchronized(obj) { 
// 需要锁定的代码
}

互斥锁的优缺点

  • 优点:能有效防止因多线程抢夺资源造成的数据安全问题
  • 缺点:需要消耗大量的 CPU 资源

使用前提:多条线程抢夺同一块资源时使用。

互斥锁在 iOS 开发中的使用

  • atomic:原子属性,为 setter 方法加锁。线程安全,需要消耗大量资源。
  • nonatomic:非原子属性,不会未 setter 方法加锁,非线程安全,适合内存小的移动设备。

建议:
所有属性都声明为 nonatomic,尽量避免多线程抢夺同一块资源,将加锁、资源抢夺的业务逻辑交给服务端处理,减少客户端的压力。

串行和并发?

队列是用来存放任务的,由 GCD 将这些任务从队列中取出并放到相应的线程中执行。

串行队列(Serial Dispatch Queue)

让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)。

使用 dispatch_queue_create 创建串行队列

1
2
// 创建串行队列(队列类型传递 NULL 或者 DISPATCH_QUEUE_SERIAL)
dispatch_queue_t queue = dispatch_queue_create("serial_queue", NULL);

使用 dispatch_get_main_queue 获取主队列

主队列是 GCD 自带的一种特殊的串行队列:放在主队列中的任务,都会放到主线程中执行。

1
dispatch_queue_t queue = dispatch_get_main_queue();

并发队列(Concurrent Dispatch Queue)

可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务),并发功能只有在异步函数(dispatch_async)才有效。

使用 dispatch_queue_create 创建并发队列

1
2
// 创建并发队列(队列类型传递 DISPATCH_QUEUE_CONCURRENT)
dispatch_queue_t queue = dispatch_queue_create("concurrent.queue", DISPATCH_QUEUE_CONCURRENT);

使用 dispatch_get_global_queue 获得全局并发队列

GCD 默认已经提供了全局的并发队列,供整个应用使用,可以无需手动创建。

1
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

队列和线程

  • 队列是用来存放任务的“暂存区”
  • 线程是任务的“执行路径”

GCD 将这些存在于队列的任务取出来放到相应的线程上去执行,而队列的性质决定了其中的任务在哪种线程上执行。

同步和异步?

  • 同步线程:dispatch_sync,同一个线程内执行任务。
  • 异步线程:dispatch_async,开辟线程来执行任务。

并发和并行?

并发:

当有多个线程操作时,如果系统只有一个 CPU,则它根本不可能真正同时进行一个以上的线程操作,它只能把 CPU 运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在时间段的线程代码运行时,其他线程处于挂起状态。

并行:

当系统有一个及以上的 CPU 时,当一个 CPU 执行一个线程时,另一个 CPU 可以执行另一个线程,两个线程互不强占 CPU 资源,可以同时进行。

子线程与主线程的通信?

需求点:我们有时候需要在子线程处理一个耗时比较长的任务,而且此任务完成后,要在主线程执行另一个任务。
例如:从网络加载图片(子线程),加载完成需要更新 UIImageView(主线程)。
实现:拿到全局并发队列(或自己开启一个子线程)来执行耗时操作,然后在其完成 Block 中拿到主队列来刷新 UI 的任务。

1
2
3
4
5
6
7
8
9
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 加载图片
NSData *dataFromURL = [NSData dataWithContentsOfURL:imageURL];
UIImage *imageFromData = [UIImage imageWithData:dataFromURL];
dispatch_async(dispatch_get_main_queue(), ^{
// 加载完成更新 UI
UIImageView *imageView = [[UIImageView alloc] initWithImage:imageFromData];
});
});

除了复杂算法,网络请求以外,大多数 dataWithContentsOf 函数也可能会比较耗时,建议遇到与 NSData 交互操作时,尽量将其放到子线程执行。

GCD 常用函数?

dispatch_once

单例。

需求点:用于在程序启动到终止,只执行一次代码。此代码被执行后,相当于自身全部被加上了注释,不会再执行。

实现:使用 dispatch_once 让代码在运行一次后即刻被“雪藏”。

1
2
3
4
5
// 使用 dispatch_once 函数能保证某段代码在程序运行过程中只被执行 1 次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行 1 次的代码,这里默认是线程安全的:不会有其他线程可以访问到这里
});

dispatch_group

图片分段下载。

需求点:执行多个耗时的异步任务,但是只能等到这些任务都执行完毕后,才能在主线程执行某个任务。

实现:把这些异步执行的操作放在 dispatch_group_async 函数中执行,最后再调用 dispatch_group_notify 来执行最后执行的任务。

1
2
3
4
5
6
7
8
9
10
dispatch_group_t group =  dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行1个耗时的异步操作
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行1个耗时的异步操作
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步操作都执行完毕后,回到主线程...
});

dispatch_barrier

需求点:虽然外面有时要执行几个不同的异步操作,但是我们还是要将其分成 2 组。当第 1 组异步任务都执行完成后,才执行第 2 组的异步任务。这里的组可以包含 1 个或者多个任务。

实现:使用 dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block); 在两组任务之间形成“栅栏”,使其“下方”的异步任务在其“上方”的异步任务都完成之前是无法执行的。

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
dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
for (NSInteger index = 0; index < 10000; index ++) {}
NSLog(@"完成了任务1");
});
dispatch_async(queue, ^{
for (NSInteger index = 0; index < 20000; index ++) {}
NSLog(@"完成了任务2");
});
dispatch_async(queue, ^{
for (NSInteger index = 0; index < 200000; index ++) {}
NSLog(@"完成了任务3");
});

dispatch_barrier_async(queue, ^{
NSLog(@"--------我是分割线--------");
});

dispatch_async(queue, ^{
for (NSInteger index = 0; index < 400000; index ++) {}
NSLog(@"完成了任务4");
});
dispatch_async(queue, ^{
for (NSInteger index = 0; index < 1000000; index ++) {}
NSLog(@"完成了任务5");
});
dispatch_async(queue, ^{
for (NSInteger index = 0; index < 1000; index ++) {}
NSLog(@"完成了任务6");
});

dispatch_semaphore

信号量控制并发,通过“计数”的方式来标示线程是否是等待或继续执行的。

  • dispatch_semaphore_create 创建一个信号
  • dispatch_semaphore_signal 发送一个信号
  • dispatch_semaphore_wait  等待信号
1
2
3
4
5
6
7
8
9
10
11
12
__block BOOL isOK = NO;  
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
Engine *engine = [[Engine alloc] init];
[engine queryCompletion:^(BOOL isOpen) {
isOK = isOpen;
dispatch_semaphore_signal(semaphore);
} onError:^(int errorCode, NSString *errorMessage) {
isOK = NO;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 等待 query 完成才会执行之后代码

Reference