一、什么是 GCD
GCD
是 Grand Central Dispatch
的简称,它是基于 C
语言的。如果使用GCD
完全由系统管理线程,不需要编写线程代码。只需定义想要执行的任务,然后添加到适当的调度队列 dispatch queue
。GCD
会负责创建线程和调度你的任务,系统直接提供线程管理。
二、GCD 任务和队列
首先看下这段代码:
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
上面的这段代码是一个简单的异步任务,通过这段代码,引出了下面的几个名词:
1、异步执行(async
)与同步执行(sync
):
同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
只能在当前线程中执行任务,不具备开启新线程的能力。异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
可以在新的线程中执行任务,具备开启新线程的能力;
异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。
2、队列
队列的基本原理:先进先出(FIFO
)的原则,先进队列的元素先出队列。
在 GCD
中,常见的两种队列:串行队列和并发队列;
串行队列(Serial Dispatch Queue):
串行队列中,只开启一个线程,一个任务执行完毕之后,在执行下一个任务;
并发队列(Concurrent Dispatch Queue):
并发队列中,可以开启多个线程,多个任务同时并发执行;
三、GCD 的使用
1、队列的创建方法 / 获取方法
串行队列(Serial Dispatch Queue)和并发队列(Concurrent Dispatch Queue)
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("com.serial.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("com.concurrent.testQueue", DISPATCH_QUEUE_CONCURRENT);
- 第一个参数表示队列的唯一标识符,用于 DEBUG,可为空。队列的名称推荐使用应用程序 ID 这种逆序全程域名。
- 第二个参数用来识别是串行队列还是并发队列。
DISPATCH_QUEUE_SERIAL
表示串行队列,DISPATCH_QUEUE_CONCURRENT
表示并发队列。
主队列(Main Dispatch Queue)
// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();
全局并发队列(Global Dispatch Queue)
// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
通过 dispatch_get_global_queue
方法获取的全局队列都是并行队列,并且队列不能被修改。
identifier
:用以标识队列优先级;
flags
:苹果预留的,第二个参数暂时没用,用 0 即可;
2、任务和队列不同组合方式的区别
区别 | 并发队列 | 串行队列 | 主队列 |
---|---|---|---|
同步(sync) | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 会发生死锁,造成 crash |
异步(async) | 有开启新线程,并发执行任务 | 有开启新线程(1条),并发执行任务 | 没有开启新线程,串行执行任务 |
3、主队列同步造成死锁的原因
dispatch_sync
函数本身是放在主线程中执行的,也就是说他本身也是属于主线程执行任务的一部分。根据主线程的特点:主线程会等主线程上的代码执行完毕之后才会去执行放置到主队列中的 task
;再根据 disptach_sync
函数特点, task
不执行完毕,dispatch_sync
函数不返回。这样,dispatch_sync
为了返回会等 task
执行完毕也就是主线程执行完,而 task
执行又等着主线程上的代码执行完,也即主线程上 dispatch_sync
代码执行完。两个任务互相等待,造成死锁;
/**
主队列同步
*/
- (void)syncMain {
NSLog(@"\n\n**************主队列同步,放到主线程会死锁***************\n\n");
// 主队列
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"主队列同步1 %@",[NSThread currentThread]);
}
});
}
4、同步执行 + 并发队列
只会在当前线程中依次执行任务,不会开启新线程,执行完一个任务,再执行下一个任务,按照1>2>3顺序执行,遵循 FIFO 原则。
- (void)testConcurrentQueueAsynExecution {
// 并发队列
dispatch_queue_t queue = dispatch_queue_create("com.test.testQueue", DISPATCH_QUEUE_CONCURRENT);
// 第一个任务
dispatch_sync(queue, ^{
// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"----执行第一个任务---当前线程%@", [NSThread currentThread]);
});
// 第二个任务
dispatch_sync(queue, ^{
// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"----执行第二个任务---当前线程%@", [NSThread currentThread]);
});
// 第三个任务
dispatch_sync(queue, ^{
// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"----执行第三个任务---当前线程%@", [NSThread currentThread]);
});
NSLog(@"----end-----当前线程---%@", [NSThread currentThread]);
}
虽然 并发队列
可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务
不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务
需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。
5、异步执行 + 并发队列
可以开启多个线程,任务交替(同时)执行;
- (void)testConcurrentQueueSyncExecution {
// dispatch_queue_t queue = dispatch_queue_create("com.test.testQueue", DISPATCH_QUEUE_CONCURRENT);
// 全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 第一个任务
dispatch_async(queue, ^{
// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"----执行第一个任务---当前线程%@", [NSThread currentThread]);
});
// 第二个任务
dispatch_async(queue, ^{
// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"----执行第二个任务---当前线程%@", [NSThread currentThread]);
});
// 第三个任务
dispatch_async(queue, ^{
// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"----执行第三个任务---当前线程%@", [NSThread currentThread]);
});
NSLog(@"----end-----当前线程---%@", [NSThread currentThread]);
}
从 log 中可以发现,系统另外开启了3条线程,并且任务是同时执行的,并不是按照1>2>3顺序执行。所以异步+并发队列具备开启新线程的能力,且并发队列可开启多个线程,同时执行多个任务。
6、同步执行 + 串行队列
只会在当前线程中依次执行任务,不会开启新线程,执行完一个任务,再执行下一个任务,按照1>2>3顺序执行,遵循 FIFO 原则,并且不会产生新的线程。
- (void)testSerialQueueAsynExecution {
dispatch_queue_t queue = dispatch_queue_create("com.test.syncQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"任务一");
NSLog(@"currentThread:%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"任务二");
NSLog(@"currentThread:%@", [NSThread currentThread]);
});
NSLog(@"任务三");
}
7、异步执行 + 串行队列
开启了一条新线程,异步执行具备开启新线程的能力且只开启一个线程,在该线程中执行完一个任务,再执行下一个任务,按照1>2>3顺序执行,遵循 FIFO 原则。
- (void)testSerialQueueSyncExecution {
dispatch_queue_t queue = dispatch_queue_create("com.test.syncQueue", DISPATCH_QUEUE_SERIAL);
// 第一个任务
dispatch_async(queue, ^{
// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"----执行第一个任务---当前线程%@", [NSThread currentThread]);
});
// 第二个任务
dispatch_async(queue, ^{
// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"----执行第二个任务---当前线程%@", [NSThread currentThread]);
});
// 第三个任务
dispatch_async(queue, ^{
// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"----执行第三个任务---当前线程%@", [NSThread currentThread]);
});
}
8、同步执行 + 主队列
直接crash,这是因为发生了死锁,在 GCD 中,禁止在主队列(串行队列)中再以同步操作执行主队列任务。同理,在同一个同步串行队列中,再使用该队列同步执行任务也是会发生死锁。
- (void)testMainQueueAsynExecution {
dispatch_queue_t queue = dispatch_queue_create("com.test.testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"----11111-----当前线程%@", [NSThread currentThread]);//到这里就死锁了
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"----22222---当前线程%@", [NSThread currentThread]);
});
NSLog(@"----333333-----当前线程%@", [NSThread currentThread]);
});
NSLog(@"----44444-----当前线程%@", [NSThread currentThread]);
}
9、异步执行 + 主队列
所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然异步执行具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中),在主线程中执行完一个任务,再执行下一个任务,按照1>2>3顺序执行,遵循 FIFO 原则。
- (void)testMainQueueSyncExecution {
// 获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 第一个任务
dispatch_async(queue, ^{
// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"----执行第一个任务---当前线程%@", [NSThread currentThread]);
});
// 第二个任务
dispatch_async(queue, ^{
// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"----执行第二个任务---当前线程%@", [NSThread currentThread]);
});
// 第三个任务
dispatch_async(queue, ^{
// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];
NSLog(@"----执行第三个任务---当前线程%@", [NSThread currentThread]);
});
NSLog(@"----end-----当前线程---%@", [NSThread currentThread]);
}
四、GCD 线程之间的通讯
- (void)communication {
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 异步追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
// 回到主线程
dispatch_async(mainQueue, ^{
// 追加在主线程中执行的任务
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
});
}
五、GCD 的其他方法
1、dispatch_after
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
需要注意的是,使用 dispatch_after
实现延迟执行某动作,时间并不是很精确,因为 main dishpatch queue
在主线程的 runLoop
中执行,所以比如在每隔1/60秒执行的 RunLoop
中,block
最快在三秒后执行,最慢在3秒+1/60秒后执行,如果在 main dishpatch queue
有大量任务处理会使主线程本身的任务处理有延迟时,这个时间会增加。
如果对时间的精确度没有高要求,只是为了推迟执行,那么使用dispatch_after还是很不错的。
- NSObject中提供的线程延迟方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
- 通过 NSTimer 来延迟线程执行
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:NO];
2、dispatch_once
一般我们会利用 dispatch_once
创建单例
从上面代码中可以看出
第一个参数 predicate
,该参数是检查后面第二个参数所代表的代码块是否被调用的谓词,
第二个参数则是在整个应用程序中只会被调用一次的代码块。dispach_once
函数中的代码块只会被执行一次,而且还是线程安全的。
3、dispatch_apply
从上面代码中可以看出,这些迭代是并发执行的和普通 for
循环一样,dispatch_apply
和 dispatch_apply_f
函数也是在所有迭代完成之后才会返回,因此这两个函数会阻塞当前线程,主线程中调用这两个函数必须小心,可能会阻止事件处理循环并无法响应用户事件。所以如果循环代码需要一定的时间执行,可以考虑在另一个线程中调用这两个函数。如果你传递的参数是串行 queue
,而且正是执行当前代码的 queue
,就会产生死锁。
4、dispatch_group_t dispatch_group_notify
可以使用 dispatch_group_async
函数将多个任务关联到一个 dispatch group
和相应的 queue
中,group
会并发地同时执行这些任务。而且 dispatch group
可以用来阻塞一个线程,直到 group
关联的所有的任务完成执行。有时候你必须等待任务完成的结果,然后才能继续后面的处理。
5、dispatch_barrier_async
在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续进行,使用 dispatch_barrier_async
函数将任务加入到并行队列之后,任务会在前面任务全部执行完成之后执行,任务执行过程中,其他任务无法执行,直到 barrier
任务执行完成。
有时候我们会需要这样的一个场景,A任务和B任务执行完毕之后,在执行C任务,需要借助 dispatch_barrier_async
这个函数。
从代码中可以看出确实只有在前面A、B任务完成后,barrier 任务才能执行,最后才能执行C任务。
注意:
使用
dispatch_barrier_async
,该函数只能搭配自定义并行队列dispatch_queue_t
使用。不能使用:dispatch_get_global_queue
,否则dispatch_barrier_async
的作用会和dispatch_async
的作用一模一样。
6、信号量
个人理解,在多线程下使用信号量可以控制多线程的并发数目。
创建信号量,可以设置信号量的资源数。0 表示没有资源,调用 dispatch_semaphore_wait
会立即等待。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
等待信号,可以设置超时参数。该函数返回0表示得到通知,非0表示超时。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
通知信号,如果等待线程被唤醒则返回非0,否则返回0。
dispatch_semaphore_signal(semaphore);
比如,执行10个任务,然后等待2秒,然后继续执行。
六、Dispatch Semaphore 线程同步
有时候会遇到这样的需求:
异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。换句话说,相当于,将将异步执行任务转换为同步执行任务。比如说:AFNetworking
中 AFURLSessionManager.m
里面的 tasksForKeyPath:
方法。通过引入信号量的方式,等待异步执行任务结果,获取到 tasks
,然后再返回该 tasks
。
- (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;
}