谈到iOS多线程,一般都会谈到四种方式:pthread、NSThread、GCD和NSOperation。
Main Dispatch Queue/Global Dispatch Queue 主队列特殊的串行、全局的并行队列
全局的并行队列dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"并行队列任务1");
主队列特殊的串行dispatch_sync(dispatch_get_main_queue(), ^{
/**
* 主线程执行
*/
});
});
//串行队列
dispatch_queue_t queue = dispatch_queue_create("com.example.MyQueue", DISPATCH_QUEUE_SERIAL);
//并行队列
dispatch_queue_t queue = dispatch_queue_create("com.example.MyQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_suspend并不会立即暂停正在运行的block,而是在当前block执行完成后,暂停后续的block执行。
dispatch_after是延迟提交,不是延迟运行 就是入队,指的就是将一个Block在特定的延时以后,加入到指定的队列中,不是在特定的时间后立即运行!。
dispatch_queue_t queue = dispatch_queue_create("me.tutuge.test.gcd", DISPATCH_QUEUE_SERIAL);
//提交第一个block,延时5秒打印。
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:5];
NSLog(@"After 5 seconds...");
});
灵活使用dispatch_group
很多时候我们需要等待一系列任务(block)执行完成,然后再做一些收尾的工作。
使用dispatch_barrier_async,dispatch_barrier_sync的注意事项
dispatch_barrier_async的作用就是向某个队列插入一个block,当目前正在执行的block运行完成后,阻塞这个block后面添加的block,只运行这个block直到完成,然后再继续后续的任务,有点“唯我独尊”的感觉=。=
值得注意的是:
dispatchbarrier\(a)sync只在自己创建的并发队列上有效,在全局(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样。
既然在串行队列上跟dispatch_(a)sync效果一样,那就要小心别死锁!
dispatch_set_context与dispatch_set_finalizer_f的配合使用
dispatch_set_context可以为队列添加上下文数据,但是因为GCD是C语言接口形式的,所以其context参数类型是“void *”。也就是说,我们创建context时有如下几种选择:
用C语言的malloc创建context数据。
用C++的new创建类对象。
用Objective-C的对象,但是要用__bridge等关键字转为Core Foundation对象。
以上所有创建context的方法都有一个必须的要求,就是都要释放内存!,无论是用free、delete还是CF的CFRelease,我们都要确保在队列不用的时候,释放context的内存,否则就会造成内存泄露。
Main Dispatch Queue是在主线程执行的dispatch queue, Main Dispatch Queue是一个Serail Dispatch Queue。追加到Main Dispatch Queue的处理在主线程的RunLoop中执行。一般将用户界面更新等必需要在主线程中执行的处理追加到Main Dispatch Queue中。
Global Dispatch Queue是所有应用程序都能过使用的Concurrent Dispatch Queue。没有必要通过dispatch_queue_create函数逐个创建Concurrent Dispatch Queue,只要获取Global Dispatch Queue使用即可。Global Dispatch Queue有四个优先级
会引起锁死 该段代码在主线程中执行指定的Block,并等待其执行结束。而其实主线程正在执行此段代码,所以无法执行追加到Mian Dispatch Queue中的Block。
对于锁死的简单理解:一般在串行队列中 如主线程mian 在主线程在执行一个sync同步线程 主队列在执行 当前的代码 无法添加这个同步block到Queue中
在main线程使用“同步”方法提交Block,必定会死锁。
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"I am block...");
});
GCD有三种队列类型:
串行队列,串行队列将任务以先进先出(FIFO)的顺序来执行,所以串行队列经常用来做访问某些特定资源的同步处理。你可以也根据需要创建多个队列,而这些队列相对其他队列都是并发执行的。换句话说,如果你创建了4个串行队列,每一个队列在同一时间都只执行一个任务,对这四个任务来说,他们是相互独立且并发执行的。如果需要创建串行队列,一般用dispatch_queue_create这个方法来实现,并指定队列类型DISPATCH_QUEUE_SERIAL。
并行队列,并发队列虽然是能同时执行多个任务,但这些任务仍然是按照先到先执行(FIFO)的顺序来执行的。并发队列会基于系统负载来合适地选择并发执行这些任务。并发队列一般指的就是全局队列(Global queue),进程中存在四个全局队列:高、中(默认)、低、后台四个优先级队列,可以调用dispatch_get_global_queue函数传入优先级来访问队列。当然我们也可以用dispatch_queue_create,并指定队列类型DISPATCH_QUEUE_CONCURRENT,来自己创建一个并发队列。
主队列,与主线程功能相同。实际上,提交至main queue的任务会在主线程中执行。main queue可以调用dispatch_get_main_queue()来获得。因为main queue是与主线程相关的,所以这是一个串行队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。
注意:上面也说过,队列间的执行是并行的,但是也存在一些限制。比如,并行执行的队列数量受到内核数的限制,无法真正做到大量队列并行执行;比如,对于并行队列中的全局队列而言,其存在优先级关系,执行的时候也会遵循其优先顺序,而不是并行。
2. 任务
linux内核中的任务的定义是描述进程的一种结构体,而GCD中的任务只是一个代码块,它可以指一个block或者函数指针。根据这个代码块添加进入队列的方式,将任务分为同步任务和异步任务:
同步任务,使用dispatch_sync将任务加入队列。将同步任务加入串行队列,会顺序执行,一般不这样做并且在一个任务未结束时调起其它同步任务会死锁。将同步任务加入并行队列,会顺序执行,但是也没什么意义。
异步任务,使用dispatch_async将任务加入队列。将异步任务加入串行队列,会顺序执行,并且不会出现死锁问题。将异步任务加入并行队列,会并行执行多个任务,这也是我们最常用的一种方式。
三、GCD常见用法和应用场景
非常喜欢一句话:Talk is cheap, show me the code.接下来对GCD的使用,我会通过代码展示。
1. dispatch_async
一般用法
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
// 一个异步的任务,例如网络请求,耗时的文件操作等等
...
dispatch_async(dispatch_get_main_queue(), ^{
// UI刷新
...
});
});
应用场景
这种用法非常常见,比如开启一个异步的网络请求,待数据返回后返回主队列刷新UI;又比如请求图片,待图片返回刷新UI等等。
2. dispatch_after
一般用法
dispatch_queue_t queue= dispatch_get_main_queue();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), queue, ^{
// 在queue里面延迟执行的一段代码
...
});
应用场景
这为我们提供了一个简单的延迟执行的方式,比如在view加载结束延迟执行一个动画等等。
3. dispatch_once
一般用法
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行一次的任务
...
});
应用场景
可以使用其创建一个单例,也可以做一些其他只执行一次的代码,比如做一个只能点一次的button(好像没啥用)。
4. 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, ^{
// 异步任务1
});
dispatch_group_async(group, queue, ^{
// 异步任务2
});
// 等待group中多个异步任务执行完毕,做一些事情,介绍两种方式
// 方式1(不好,会卡住当前线程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
...
// 方式2(比较好)
dispatch_group_notify(group, mainQueue, ^{
// 任务完成后,在主队列中做一些操作
...
});
应用场景
上述的一种方式,可以适用于自己维护的一些异步任务的同步问题;但是对于已经封装好的一些库,比如AFNetworking等,我们不获取其异步任务的队列,这里可以通过一种计数的方式控制任务间同步,下面为解决单界面多接口的一种方式。
// 两个请求和参数为我项目里面的不用在意。
// 计数+1
dispatch_group_enter(group);
[JDApiService getActivityDetailWithActivityId:self.activityId Location:stockAddressId SuccessBlock:^(NSDictionary *userInfo) {
// 数据返回后一些处理
...
// 计数-1
dispatch_group_leave(group);
} FailureBlock:^(NSError *error) {
// 数据返回后一些处理
...
// 计数-1
dispatch_group_leave(group);
}];
// 计数+1
dispatch_group_enter(group);
[JDApiService getAllCommentWithActivityId:self.activityId PageSize:3 PageNum:self.commentCurrentPage SuccessBlock:^(NSDictionary *userInfo) {
// 数据返回后一些处理
...
// 计数-1
dispatch_group_leave(group);
} FailureBlock:^(NSError *error) {
// 数据返回后一些处理
...
// 计数-1
dispatch_group_leave(group);
}];
// 其实用计数的说法可能不太对,但是就这么理解吧。会在计数为0的时候执行dispatch_group_notify的任务。
dispatch_group_notify(group, mainQueue, ^{
// 一般为回主队列刷新UI
...
});
5. dispatch_barrier_async
一般用法
// dispatch_barrier_async的作用可以用一个词概括--承上启下,它保证此前的任务都先于自己执行,此后的任务也迟于自己执行。本例中,任务4会在任务1、2、3都执行完之后执行,而任务5、6会等待任务4执行完后执行。
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 任务1
...
});
dispatch_async(queue, ^{
// 任务2
...
});
dispatch_async(queue, ^{
// 任务3
...
});
dispatch_barrier_async(queue, ^{
// 任务4
...
});
dispatch_async(queue, ^{
// 任务5
...
});
dispatch_async(queue, ^{
// 任务6
...
});
应用场景
和dispatch_group类似,dispatch_barrier也是异步任务间的一种同步方式,可以在比如文件的读写操作时使用,保证读操作的准确性。另外,有一点需要注意,dispatch_barrier_sync和dispatch_barrier_async只在自己创建的并发队列上有效,在全局(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样。
6. dispatch_apply
一般用法
// for循环做一些事情,输出0123456789
for (int i = 0; i < 10; i ++) {
NSLog(@"%d", i);
}
// dispatch_apply替换(当且仅当处理顺序对处理结果无影响环境),输出顺序不定,比如1098673452
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*! dispatch_apply函数说明
*
* @brief dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API
* 该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等到全部的处理执行结束
*
* @param 10 指定重复次数 指定10次
* @param queue 追加对象的Dispatch Queue
* @param index 带有参数的Block, index的作用是为了按执行的顺序区分各个Block
*
*/
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu", index);
});
应用场景
那么,dispatch_apply有什么用呢,因为dispatch_apply并行的运行机制,效率一般快于for循环的类串行机制(在for一次循环中的处理任务很多时差距比较大)。比如这可以用来拉取网络数据后提前算出各个控件的大小,防止绘制时计算,提高表单滑动流畅性,如果用for循环,耗时较多,并且每个表单的数据没有依赖关系,所以用dispatch_apply比较好。
7. dispatch_suspend和dispatch_resume
一般用法
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_suspend(queue); //暂停队列queue
dispatch_resume(queue); //恢复队列queue
应用场景
这种用法我还没有尝试过,不过其中有个需要注意的点。这两个函数不会影响到队列中已经执行的任务,队列暂停后,已经添加到队列中但还没有执行的任务不会执行,直到队列被恢复。
8. dispatch_semaphore_signal
一般用法
// dispatch_semaphore_signal有两类用法:a、解决同步问题;b、解决有限资源访问(资源为1,即互斥)问题。
// dispatch_semaphore_wait,若semaphore计数为0则等待,大于0则使其减1。
// dispatch_semaphore_signal使semaphore计数加1。
// a、同步问题:输出肯定为1、2、3。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore1 = dispatch_semaphore_create(1);
dispatch_semaphore_t semaphore2 = dispatch_semaphore_create(0);
dispatch_semaphore_t semaphore3 = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
// 任务1
dispatch_semaphore_wait(semaphore1, DISPATCH_TIME_FOREVER);
NSLog(@"1\n");
dispatch_semaphore_signal(semaphore2);
dispatch_semaphore_signal(semaphore1);
});
dispatch_async(queue, ^{
// 任务2
dispatch_semaphore_wait(semaphore2, DISPATCH_TIME_FOREVER);
NSLog(@"2\n");
dispatch_semaphore_signal(semaphore3);
dispatch_semaphore_signal(semaphore2);
});
dispatch_async(queue, ^{
// 任务3
dispatch_semaphore_wait(semaphore3, DISPATCH_TIME_FOREVER);
NSLog(@"3\n");
dispatch_semaphore_signal(semaphore3);
});
// b、有限资源访问问题:for循环看似能创建100个异步任务,实质由于信号限制,最多创建10个异步任务。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);
for (int i = 0; i < 100; i ++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
// 任务
...
dispatch_semaphore_signal(semaphore);
});
}
应用场景
其实关于dispatch_semaphore_t,并没有看到太多应用和资料解释,我只能参照自己对linux信号量的理解写了两个用法,经测试确实相似。这里,就不对一些死锁问题进行讨论了。
9. dispatch_set_context、dispatch_get_context和dispatch_set_finalizer_f
一般用法
// dispatch_set_context、dispatch_get_context是为了向队列中传递上下文context服务的。
// dispatch_set_finalizer_f相当于dispatch_object_t的析构函数。
// 因为context的数据不是foundation对象,所以arc不会自动回收,一般在dispatch_set_finalizer_f中手动回收,所以一般讲上述三个方法绑定使用。
- (void)test
{
// 几种创建context的方式
// a、用C语言的malloc创建context数据。
// b、用C++的new创建类对象。
// c、用Objective-C的对象,但是要用__bridge等关键字转为Core Foundation对象。
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
if (queue) {
// "123"即为传入的context
dispatch_set_context(queue, "123");
dispatch_set_finalizer_f(queue, &xigou);
}
dispatch_async(queue, ^{
char *string = dispatch_get_context(queue);
NSLog(@"%s", string);
});
}
// 该函数会在dispatch_object_t销毁时调用。
void xigou(void *context)
{
// 释放context的内存(对应上述abc)
// a、CFRelease(context);
// b、free(context);
// c、delete context;
}