我的 GCD 回顾

入门

GCD 对我来说是初入门 iOS 开发的记忆,那个时候还不知道 UIKit 中的类应该在主线程调用,所以异步获取了数据之后虽然调用了 reloadData,但是界面怎么都不刷新,心慌慌查了好久才解决这个问题,现在回想起来也是蛮有趣。这可以说明 GCD 的引入,真的大大降低了进行多线程操作的门槛,然而要解决多线程带来的难题却依旧不那么容易。

引入题外话:为什么要在主线程更新 UI?

我的理解是:要在主线程更新 UI 不是一种技术限制,而更像是一种约定。系统 SDK 平台都约定好在这个环境中进行开发时应在主线程更新UI。主线程和其他线程并没有本质的区别,只是碰巧 UIApplicatin 在这个线程初始化。那么为什么要进行这种约定?我觉得最重要的原因应是性能考虑,如果要将 UIKit 全部做成线程安全,势必要增加保证线程安全的逻辑,这部分的性能再好,相对于约定只在主线程更新UI的方式,始终是额外的负担。

GCD 入门

从我开始做 iOS 开发以来从没有在项目中直接使用过 NSThread 对象,所以一直没有什么直观的印象,直到简单了解了 pthread 之后才清晰的认识到 NSThread 其实是对 pthread 的简单封装面向对象化,在 iOS 10 中 NSThread 新增了使用 block 直接初始化的方法,这才使得 NSThread 的使用简化了不少,而在这之前和 pthread 一样不够优雅。在 pthread 中使用 pthread_create 创建线程,要传入函数指针作为回调,早先的 NSThread 则是标准的 cocoa 风格 target selector。

pthread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void *PrintHello(void *threadid)
{
long tid;
tid = (long)threadid;
printf("Hello pthread #%ld!\n", tid);
pthread_exit(NULL);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
pthread_t threads[5];
int rc;
long t;
for (t=0; t<5; t++){
rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t);
}
}
return 0;
}

NSThread

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, const char * argv[]) {
@autoreleasepool {
ThreadTarget *target = [ThreadTarget new];
long t;
for (t=0; t<5; t++) {
[NSThread detachNewThreadSelector:@selector(hello:) toTarget:target withObject:@(t)];
}
sleep(3);
}
return 0;
}

GCD

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc, const char * argv[]) {
@autoreleasepool {
long t;
for (t=0; t<5; t++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
printf("Hello pthread #%ld!\n", t);
});
}
sleep(3);
}
return 0;
}

NSThread 和 GCD 版本为了能打印出来,需要让 main 函数等一等。可以看出来 GCD 的使用毫无疑问是最方便的。

NSThread 对 pthread 的封装只是胶水式的薄薄封装,并没有提供其他并发相关功能。真正实现更高级封装的是 NSOperationQueue 和 GCD,NSOperationQueue 在引入 GCD 之后应该是使用 GCD 重写了,所以 iOS8 新增了 underlyingQueue 这个属性,返回一个 dispatch_queue_t。我曾经在面试里被鄙视过:GCD?GCD 能实现 NSOperation 的依赖关系吗?那个时候我没有回答上来,如果再给我一次回答的机会,我会说:可以,只是麻烦一些。这就要说到一些 GCD 的进阶功能。

GCD 进阶

面向对象是一种提高抽象能力很好的手段,以数据的形式封装掉了很多细节的逻辑。要实现 NSOperation 的依赖关系,可以使用 dispatch_group。SDK 的头文件是这样介绍的:A group of blocks submitted to queues for asynchronous invocation。将一组 block 作为一个“对象”来管理(dispatch_group_t 当然并不是一个对象,而是一个和 ObjC 对象很像的结构体,可以通过配置由 ARC 管理生命周期),常用的方法有两对 dispatch_group_wait dispatch_group_notify 和 dispatch_group_enter dispatch_group_leave。在增加依赖的时候进行 group_enter,在依赖的 block 执行完后 group_leave,这就是 swift forelimbs foundation 中的实现方式。dispatch_group 所实现的组管理,类似于 pthread_join。

pthread_join

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
void *BusyWork(void *t)
{
sleep(3);
printf("Hello pthread #%ld!\n", (long)t);
pthread_exit((void*) t);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
pthread_t thread[10];
pthread_attr_t attr;
int rc;
long t;
void *status;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
for(t=0; t<10; t++) {
rc = pthread_create(&thread[t], &attr, BusyWork, (void *)t);
}
pthread_attr_destroy(&attr);
for(t=0; t<10; t++) {
rc = pthread_join(thread[t], &status);
}
printf("finally\n");
}

dispatch_group

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(int argc, const char * argv[]) {
@autoreleasepool {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.fengweizhou.queue", DISPATCH_QUEUE_CONCURRENT);
for (int t = 0; t<10; t++) {
dispatch_group_async(group, queue, ^{
printf("Hello gcd #%d!\n", t);
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
printf("finally\n");
}
return 0;
}

iOS8 之后 block 不再是简单的 block 了,可以是 dispatch_block_t。虽然看定义 typedef void (^dispatch_block_t)(void); 它只是一个空参数空返回 的 block,但是通过API dispatch_block_create 为 block 指定 flags,通过这个接口创建出来的 dispatch_block_t 还可以 dispatch_block_wait,dispatch_block_notify,dispatch_block_cancel,这样就解决了另一个问题:block 能像 NSOperation 一样取消吗?答案是可以。

另一个进阶 dispatch 库成员就是 dispatch_semaphore 了。在 AFNetworking 里有一处应用,使用 dispatch_semaphore 从异步的 block 里取出值同步的返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
__block NSArray *tasks = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
tasks = dataTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
tasks = uploadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
tasks = downloadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return tasks;
}

NSURLSession 的接口全部都是异步执行回调,所以要返回异步block 的参数,就必须等异步block 执行完。这个应该也是对系统 #include <semaphore.h> 的封装

其他应用

除了这些还有一些其他少见应用:

dispatch_apply 实现多线程迭代,和使用 enumerateObjectsWithOptions 时指定 NSEnumerationConcurrent 差不多,迭代过程中最好不要有共用资源。

dispatch_source_timer 和 NSTimer 不同不依赖 runloop,所以不会受 runloop mode 影响。

dispatch_set_target_queue target_queue 可以影响新创建 queue 的优先级;多个串行 queue 同一个串行 target_queue 可以实现 queue 间任务串行

其他相关

虽然说 GCD 是真正的高级封装,但是有些地方还是可以很明显的暴露出 pthread 的特点,特别是一些命名例如:
pthread_getspecific,
dispatch_queue_get_specific;
pthread_setspecific,
dispatch_queue_set_specific。
用于为 queue 做标示。为了做标示还可以通过 dispatch_queue_get_label 获取创建时的 label

用 GCD 代替加锁实现线程安全访问,例子是 FMDatabaseQueue 的实现。其实就是 GCD 的方法中进行了加锁,不然 dispatch_sync 到当前线程为什么会死锁。但好处是抽象层级更高,不用手动处理锁相关。

不同优先级的 queue 低优先级会等所有高优先级的 queue 已经分配资源开始执行后才会被分配。在 background priority queue 中还会有 disk IO 和 network IO throttle。