GCD笔记
总结一下多线程部分,最强大的无疑是GCD,那么先从这一块部分讲起.
Dispatch Queue的种类
GCD 提供有 dispatch queues 来处理代码块,这些队列管理你提供给 GCD 的任务并用 FIFO 顺序执行这些任务。这就保证了第一个被添加到队列里的任务会是队列中第一个开始的任务,而第二个被添加的任务将第二个开始,如此直到队列的终点。
所有的调度队列(dispatch queues)自身都是线程安全的,你能从多个线程并行的访问它们。当你了解了调度队列如何为你自己代码的不同部分提供线程安全后,GCD的优点就是显而易见的。关于这一点的关键是选择正确类型的调度队列和正确的调度函数来提交你的工作。
在本节你会看到两种调度队列,都是由 GCD 提供的
- Serial Dispatch Queue 等待现在执行中处理结束,也就是我们常说的
串行队列
串行队列中的任务一次执行一个,每个任务只在前一个任务完成时才开始。而且,你不知道在一个 Block 结束和下一个开始之间的时间长度.这些任务的执行时机受到 GCD 的控制;唯一能确保的事情是 GCD 一次只执行一个任务,并且按照我们添加到队列的顺序来执行。 - Concurrent Dispatch Queue 不等待现在执行中处理结束,也就是我们常说的
并行队列
.在并发队列中的任务能得到的保证是它们会按照被添加的顺序开始执行,但这就是全部的保证了。任务可能以任意顺序完成,你不会知道何时开始运行下一个任务,或者任意时刻有多少 Block 在运行。再说一遍,这完全取决于 GCD 。
GCD众多API解析
1.Dispatch_queue_creat
- 生成一个串行队列,按照顺序执行队列里的任务
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_creat("com.example.gcd.MyConcurrentDispatchQueue",NULL);
- 生成一个并行队列,同时执行队列里的任务
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_creat("com.example.gcd.MyConcurrentDispatchQueue",DISPATCH_QUEUE_CONCURRENT);
注意:非ARC情况下,通过creat函数生成的队列,必须程序员自己来进行内存管理,通过
dispatch_release()
或者dispatch_retain()
来进行内存管理,ARC情况下,无须我们自己处理
2.Main Dispatch Queue/Global Dispatch Queue
Global Dispatch Queue 有4个优先级,分别是高优先级(
High Priority
) \默认优先级(Default Priority
)\低优先级(Low Priority
)和后台优先级(Background Priority
).
首先,系统提供给你一个叫做 主队列(main queue) 的特殊队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。然而,它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。这个队列就是用于发生消息给 UIView 或发送通知的。
系统同时提供给你好几个并发队列。它们叫做 全局调度队列(Global Dispatch Queues) 。目前的四个全局队列有着不同的优先级:background、low、default 以及 high。要知道,Apple 的 API 也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务。
最后,你也可以创建自己的串行队列或并发队列。这就是说,至少有五个队列任你处置:主队列、四个全局调度队列,再加上任何你自己创建的队列。
以上是调度队列的大框架!
GCD 的“艺术”归结为选择合适的队列来调度函数以提交你的工作,下面介绍一下系统提供的众多函数
3.Dispatch_set_target_queue
功能:变更生成的Dispatch Queue 的
执行优先级
要使用该函数.
4.Dispatch_after
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<#delayInSeconds#> * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ <#code to be executed after a specified delay#> });
注意:
dispatch_after
函数并不是在指定时间后执行处理,而是在指定时间追加处理到Dispatch Queue
.dispatch_after
工作起来就像一个延迟版的dispatch_async
。你依然不能控制实际的执行时间,且一旦dispatch_after
返回也就不能再取消它。
- 第一个参数是指定时间,用的
dispatch_time_t
类型的值.该值使用dispatch_time
函数或dispatch_walltime
函数作成.
dispatch_time
用于计算相对时间
dispatch_walltime
用于计算绝对时间
ull
代表unsigned long long
数值和NSEC_PER_SEC
的乘积得到单位为秒的数值
数值和NSEC_PER_MSEC
的乘积得到单位为毫秒的数值DISPATCH_TIME_NOW
表示现在的时间关于dispatch_time
,第一个参数通常使用DISPATCH_TIME_NOW
,它是一个表示dispatch_time_t
的宏,表示从现在开始算起 - 第二个参数是第一个参数之后经历的时长。而我们通常用的
NSEC_PER_SEC
也是一个宏,还有其他的宏:
#define NSEC_PER_SEC 1000000000ull //每秒有多少纳秒
#define NSEC_PER_MSEC 1000000ull //每毫秒有多少纳秒
#define USEC_PER_SEC 1000000ull //每秒有多少微秒
#define NSEC_PER_USEC 1000ull //每微秒有多少纳秒
因为1秒钟有NSEC_PER_SEC(也即1000000000)纳秒,所以NSEC_PER_SEC其实就相当于1秒,那么两秒就是2 *NSEC_PER_SEC;
同理,NSEC_PER_MSEC就相当于是1毫秒,那么2秒钟,就应该是2000 * NSEC_PER_MSEC;
USEC_PER_SEC也相当于1毫秒,2秒钟就是2000 *USEC_PER_SEC
NSEC_PER_USEC相当于 1微妙,所以要表示1秒钟就是1000000 NSEC_PER_USEC。
所以1秒后执行某段代码可以这样写:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"哈哈"); });
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1000 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ NSLog(@"嘿嘿"); });
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1000 * USEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"嗯嗯"); });
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1000000 * NSEC_PER_USEC)), dispatch_get_main_queue(), ^{ NSLog(@"呃呃"); });
这个API的作用与下面这个方法类似:
[self performSelector:@selector(testClick:) withObject:nil afterDelay:2.0];
不知道何时适合使用 dispatch_after ?
- 自定义串行队列:在一个自定义串行队列上使用 dispatch_after 要小心。你最好坚持使用主队列。
- 主队列(串行):是使用 dispatch_after 的好选择;Xcode 提供了一个不错的自动完成模版。
- 并发队列:在并发队列上使用 dispatch_after 也要小心;你会这样做就比较罕见。还是在主队列做这些操作吧。
5.Dispatch Group
使用场景:在多个异步任务全部执行完毕后,执行某个任务。如果用同步任务或串行队列,就没有意义了,要谨记。
这里有两种实现方式:
方式一 利用dispatch_group_async
和dispatch_group_notify
配合.
方式二 利用dispatch_group_enter
、dispatch_group_leave
和
dispatch_group_notify
配合,其中需要注意的是有dispatch_group_enter
就必定有一个dispatch_group_leave
与之对应,否则可能会出现令你意想不到的崩溃。
Dispatch Group 会在整个组的任务都完成时通知你。这些任务可以是同步的,也可以是异步的,即便在不同的队列也行。而且在整个组的任务都完成时,Dispatch Group 可以用同步的或者异步的方式通知你。因为要监控的任务在不同队列,那就用一个 dispatch_group_t
的实例来记下这些不同的任务。
当组中所有的事件都完成时,GCD 的 API 提供了两种通知方式。
第一种是 dispatch_group_wait
,它会阻塞当前线程,直到组里面所有的任务都完成或者等到某个超时发生。这恰好是你目前所需要的。
在我们转向另外一种使用 Dispatch Group 的方式之前,先看一个简要的概述,关于何时以及怎样使用有着不同的队列类型的 Dispatch Group :
- 自定义串行队列:它很适合当一组任务完成时发出通知。
- 主队列(串行):它也很适合这样的情况。但如果你要同步地等待所有工作地完成,那你就不应该使用它,因为你不能阻塞主线程。然而,异步模型是一个很有吸引力的能用于在几个较长任务(例如网络调用)完成后更新 UI 的方式。
- 并发队列:它也很适合 Dispatch Group 和完成时通知。
dispatch_group_notify
以异步的方式工作。当 Dispatch Group 中没有任何任务时,它就会执行其代码,那么 completionBlock 便会运行。你还指定了运行 completionBlock 的队列,此处,主队列就是你所需要的。
Dispatch_group_wait
该API依然是与dispatch_group配合使用。它会阻塞当前所在的线程,直到前面的blocks 执行完成,或者超时的时候返回。该方法会同步的等待之前比较的block 对象们完成,如果在给定的时间内没有完成,该方法就会返回。如果在给定的时间超时前完成,则返回0,否则就返回一个非零的值。
示例
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 5; i++)
{
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:i];
NSLog(@"并发%d结束----线程:%@", i,[NSThread currentThread]);
});
}
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
long result = dispatch_group_wait(group, time);
if (result) {
NSLog(@"超时了");
} else {
NSLog(@"执行完毕");
}// 打印结果是:
2016-07-12 14:04:17.980 PractiseProject[4537:155453] 并发0结束----线程:{number = 3, name = (null)}
2016-07-12 14:04:18.981 PractiseProject[4537:155441] 并发1结束----线程:{number = 4, name = (null)}
2016-07-12 14:04:19.980 PractiseProject[4537:155338] 超时了
2016-07-12 14:04:19.981 PractiseProject[4537:155456] 并发2结束----线程:{number = 5, name = (null)}
2016-07-12 14:04:20.985 PractiseProject[4537:155464] 并发3结束----线程:{number = 6, name = (null)}
2016-07-12 14:04:21.984 PractiseProject[4537:155453] 并发4结束----线程:{number = 3, name = (null)}
由于dispatch_group_wait
会阻塞线程,在dispatch_group_wait
后面的代码并不会执行,如果我们在主线程中执行上面的代码段,则会阻塞UI界面。所以我们应该在子线程中执行上面的代码片段(用一个dispatch_async
包起来)。如果我们设置wait的时间为永远的话,由于在子线程中执行的任务总有结束的时候,那么dispatch_group_wait
之后执行的代码就效果就与上一篇中的dispatch_group_notify
的功能类似啦。
示例:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
dispatch_group_t group = dispatch_group_create();
for (int i = 0; i < 5; i++) {
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:i];
NSLog(@"并发%d结束----线程:%@", i,[NSThread currentThread]);
} );
}
long result = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
if (result) {
NSLog(@"超时了");
} else {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"执行完毕,回主线程更新UI");
});
}
});
6.Dispatch_barrier_async和Dispatch_barrier_sync
这个函数的意思是:前面的任务执行结束后它才执行,而且它后面的任务等它执行完毕之后才能执行.
该函数同
dispatch_queue_creat
函数生成的Concurrent Dispatch Queue
一起使用.
注意:
1.从Xcode 4开始,我们定义property后,编译器会自动帮我们添加@synthesize
,但是如果我们同时重写setter和getter,那么编译器便不再帮我们添加@synthesize
,我们需要自己添加@synthesize
。
2.dispatch_barrier_async
只能使用dispatch_queue_create
创建的并发队列,才能正确发挥它的作用。
dispatch_barrier_sync
与dispatch_barrier_async
的功能基本一致,不同之处是,dispatch_barrier_sync
是在当前线程中执行block中的任务,而dispatch_barrier_async
则是在新的线程(有可能是之前使用过的子线程)中执行任务。 它们都是在用dispatch_queue_create
创建的并发队列上有效果,而在串行队列或者dispatch_get_global_queue
创建的并发队列中,作用与dispatch_sync
一致。dispatch_barrier
决定的只是它的任务是否在新的线程中执行,以及它一定在前面几个任务执行完后执行,并不会影响之前任务的执行顺序等。在串行队列
或者dispatch_get_global_queue
创建的并发队列中,dispatch_barrier_sync
仅仅相当于dispatch_sync
dispatch_barrier_async
函数会等待追加到Concurrent Dispatch Queue
上的并行执行的处理全部结束之后,再将指定的处理追加到该Concurrent Dispatch Queue
中.然后在由dispatch_barrier_async
函数追加的处理执行完毕后,Concurrent Dispatch Queue
才恢复一般的动作,追加到该Concurrent Dispatch Queue
的处理又开始并行执行.
下面是你何时会——和不会——使用障碍函数的情况:
- 自定义串行队列:一个很坏的选择;障碍不会有任何帮助,因为不管怎样,一个串行队列一次都只执行一个操作。
- 全局并发队列:要小心;这可能不是最好的主意,因为其它系统可能在使用队列而且你不能垄断它们只为你自己的目的。
- 自定义并发队列:这对于原子或临界区代码来说是极佳的选择。任何你在设置或实例化的需要线程安全的事物都是使用障碍的最佳候选。
由于上面唯一像样的选择是自定义并发队列,你将创建一个你自己的队列去处理你的障碍函数并分开读和写函数。且这个并发队列将允许多个多操作同时进行。
7.Dispatch_sync同步函数和Dispatch_async异步函数
dispatch_sync
添加任务到一个队列并等待直到任务完成。dispatch_async
做类似的事情,但不同之处是它不会等待任务的完成,而是立即继续“调用线程”的其它任务。
特别注意:在同步函数执行主线程队列的任务会发生死锁,特别注意同步函数的死锁情况
下面是一个快速总览,关于在何时以及何处使用 dispatch_sync :
- 自定义串行队列:在这个状况下要非常小心!如果你正运行在一个队列并调用dispatch_sync 放在同一个队列,那你就百分百地创建了一个死锁。
- 主队列(串行):同上面的理由一样,必须非常小心!这个状况同样有潜在的导致死锁的情况。
- 并发队列:这才是做同步工作的好选择,不论是通过调度障碍,或者需要等待一个任务完成才能执行进一步处理的情况。
下面是一个关于在 dispatch_async 上如何以及何时使用不同的队列类型的快速指导:
- 自定义串行队列:当你想串行执行后台任务并追踪它时就是一个好选择。这消除了资源争用,因为你知道一次只有一个任务在执行。注意若你需要来自某个方法的数据,你必须内联另一个 Block 来找回它或考虑使用 dispatch_sync。
- 主队列(串行):这是在一个并发队列上完成任务后更新 UI 的共同选择。要这样做,你将在一个 Block 内部编写另一个 Block 。以及,如果你在主队列调用 dispatch_async 到主队列,你能确保这个新任务将在当前方法完成后的某个时间执行。
- 并发队列:这是在后台执行非 UI 工作的共同选择。
| 函数\队列 | 并发队列 | 自己创建的串行队列 | 主队列 |
| 同步 | 不会创建新线程,串行执行 | 不会创建新线程,串行执行 |不会创建新线程,串行执行(可能会发生死锁,如果最外层函数也是在主线程执行)|
| 异步 | 会创建多条新线程,同时执行多个任务 | 会创建新线程,串行执行任务 |不会创建新线程,串行执行|
8.Dispatch_apply
dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API.该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束.
dispatch_apply 表现得就像一个 for 循环,但它能并发地执行不同的迭代。这个函数是同步的,所以和普通的 for 循环一样,它只会在所有工作都完成后才会返回。
那何时才适合用 dispatch_apply 呢?
- 自定义串行队列:串行队列会完全抵消 dispatch_apply 的功能;你还不如直接使用普通的 for 循环。
- 主队列(串行):与上面一样,在串行队列上不适合使用 dispatch_apply 。还是用普通的 for 循环吧。
- 并发队列:对于并发循环来说是很好选择,特别是当你需要追踪任务的进度时。
该方法会等apply 中多次迭代调用的block全都执行完成后,才会返回,所以dispatch_apply会阻塞当前线程,我们得避免在主线程中使用dispatch_apply。另外,说明中已经说的很清楚了,如果我们使用dispatch_get_global_queue创建的串行队列,那么传入的block任务是并发执行的。如果我们在串行队列中执行该方法,会发生死锁,所以第二个参数,千万不要传串行队列。如果我们使用dispatch_queue_create创建的并发队列,block任务依然是顺序执行的。
下面看一下示例代码以及运行结果:dispatch_apply / dispatch_queue_create /并发队列:
dispatch_queue_t queue = dispatch_queue_create("com.haley.cn", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
dispatch_apply(5, queue, ^(size_t index) {
[NSThread sleepForTimeInterval:index];
NSLog(@"并发%zu---%@",index,[NSThread currentThread]);
});
NSLog(@"done - %@",[NSThread currentThread]);
});
NSLog(@"主线程");
// 输出结果:
2016-07-12 16:09:33.856 PractiseProject[5496:207665] 主线程
2016-07-12 16:09:33.857 PractiseProject[5496:207710] 并发0---{number = 3, name = (null)}
2016-07-12 16:09:34.860 PractiseProject[5496:207710] 并发1---{number = 3, name = (null)}
2016-07-12 16:09:36.864 PractiseProject[5496:207710] 并发2---{number = 3, name = (null)}
2016-07-12 16:09:39.867 PractiseProject[5496:207710] 并发3---{number = 3, name = (null)}
2016-07-12 16:09:43.872 PractiseProject[5496:207710] 并发4---{number = 3, name = (null)}
2016-07-12 16:09:43.873 PractiseProject[5496:207710] done - {number = 3, name = (null)}
dispatch_apply / dispatch_get_global_queue:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
dispatch_apply(5, queue, ^(size_t index) {
[NSThread sleepForTimeInterval:index];
NSLog(@"并发%zu---%@",index,[NSThread currentThread]);
});
NSLog(@"done - %@",[NSThread currentThread]);
});
NSLog(@"主线程");
// 输出结果:
2016-07-12 16:15:26.634 PractiseProject[5544:210845] 主线程
2016-07-12 16:15:26.634 PractiseProject[5544:210882] 并发0---{number = 3, name = (null)}
2016-07-12 16:15:27.637 PractiseProject[5544:210887] 并发1---{number = 2, name = (null)}
2016-07-12 16:15:28.637 PractiseProject[5544:210893] 并发2---{number = 4, name = (null)}
2016-07-12 16:15:29.636 PractiseProject[5544:210899] 并发3---{number = 5, name = (null)}
2016-07-12 16:15:30.635 PractiseProject[5544:210882] 并发4---{number = 3, name = (null)}
2016-07-12 16:15:30.635 PractiseProject[5544:210893] done - {number = 4, name = (null)}
9.Dispatch_suspend(挂起)/Dispatch_resume(恢复)
10.Dispatch Semaphore
dispatch_semaphore_t信号量是一种老式的线程概念,由非常谦卑的 Edsger W. Dijkstra 介绍给世界。信号量之所以比较复杂是因为它建立在操作系统的复杂性之上。
- (void)downloadImageURLWithString:(NSString *)URLString
{
// 1
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURL *url = [NSURL URLWithString:URLString];
__unused Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *error) {
if (error) {
XCTFail(@"%@ failed. %@", URLString, error);
}
// 2
dispatch_semaphore_signal(semaphore);
}];
// 3
dispatch_time_t timeoutTime = dispatch_time(DISPATCH_TIME_NOW, kDefaultTimeoutLengthInNanoSeconds);
if (dispatch_semaphore_wait(semaphore, timeoutTime)) {
XCTFail(@"%@ timed out", URLString);
}
}
在我们使用多线程处理多个并发任务,而这多个并发任务有资源竞争的时候,就需要一种机制,在资源不够用时,让新的任务处于等待状态,当有可用资源时,等待的任务在按序依次执行。
像这一类问题除了可以用NSOperation,设置最大并发数外,还可以使用信号量。
这里涉及到的API有如下几个:
dispatch_semaphore_t dispatch_semaphore_create(long value);
创建信号量的方法,如果初始值小于0,则会返回NULL,即信号量创建失败。参数:value 表示初始的信号量个数,可以理解为资源个数,或者最大并发个数。
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
参数:第一个参数为信号量对象,第二个参数是等待超时的时间。
讲解:该方法相当于任务开始前的检查,需要注意的是该方法会阻塞当前线程。如果此时信号量的值大于0,会返回0,并且代码会继续往下执行;如果此时信号量的值等于0,那么此时该方法会阻塞当前线程,等待timeout 的时间。如果在超时的时间内,依然没有可用的资源,那么该方法会返回一个非0的值。
该方法执行时,会使信号量的值减1。
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);
该方法应该在任务执行完毕时调用,它会使信号量的值加0。
下面用一段实际代码演示GCD信号量的使用:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 在子线程中执行,防止阻塞主线程
dispatch_async(queue, ^{
// 创建一个有3个资源的信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
for (int i = 0; i < 6; i++) {
// 检测还有多少个资源,执行后会使资源数减少1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
NSLog(@"开始执行任务%d---%@",i,[NSThread currentThread]);
[NSThread sleepForTimeInterval:6 - i];
NSLog(@"完成任务%d---%@",i,[NSThread currentThread]);
//表示资源使用完毕,会使资源数加1
dispatch_semaphore_signal(semaphore);
});
}
});
// 输出结果:
2016-07-13 17:23:53.178 PractiseProject[4973:196435] 开始执行任务1---{number = 4, name = (null)}
2016-07-13 17:23:53.178 PractiseProject[4973:196436] 开始执行任务0---{number = 3, name = (null)}
2016-07-13 17:23:53.178 PractiseProject[4973:196437] 开始执行任务2---{number = 5, name = (null)}
2016-07-13 17:23:57.179 PractiseProject[4973:196437] 完成任务2---{number = 5, name = (null)}
2016-07-13 17:23:57.179 PractiseProject[4973:196437] 开始执行任务3---{number = 5, name = (null)}
2016-07-13 17:23:58.182 PractiseProject[4973:196435] 完成任务1---{number = 4, name = (null)}
2016-07-13 17:23:58.182 PractiseProject[4973:196435] 开始执行任务4---{number = 4, name = (null)}
2016-07-13 17:23:59.179 PractiseProject[4973:196436] 完成任务0---{number = 3, name = (null)}
2016-07-13 17:23:59.179 PractiseProject[4973:196436] 开始执行任务5---{number = 3, name = (null)}
2016-07-13 17:24:00.184 PractiseProject[4973:196435] 完成任务4---{number = 4, name = (null)}
2016-07-13 17:24:00.184 PractiseProject[4973:196437] 完成任务3---{number = 5, name = (null)}
2016-07-13 17:24:00.184 PractiseProject[4973:196436] 完成任务5---{number = 3, name = (null)}
11.Dispatch_once
dispatch_once
函数是保证在应用程序执行中只执行一次指定处理的API.
以线程安全的方式执行且仅执行其代码块一次。试图访问临界区(即传递给 dispatch_once 的代码)的不同的线程会在临界区已有一个线程的情况下被阻塞,直到临界区完成为止。
12.Dispatch I/O
在读取大文件时,如果将文件分成合适的大小并使用Global Dispatch Queue并列读取的话,就是使用
dispatch I/O
和Dispatch Data
13.Dispatch Source
GCD 的一个特别有趣的特性是 Dispatch Source,它基本上就是一个低级函数的 grab-bag ,能帮助你去响应或监测 Unix 信号、文件描述符、Mach 端口、VFS 节点,以及其它晦涩的东西。所有这些都超出了本教程讨论的范围,但你可以通过实现一个 Dispatch Source 对象并以一个相当奇特的方式来使用它来品尝那些晦涩的东西。
有点不知道干嘛的,没用过,可以看一下例子
GCD 深入理解:第二部分
14.Dispatch_source中的timer
dispatch_source_t 的类型有很多种:
#define DISPATCH_SOURCE_TYPE_DATA_ADD
#define DISPATCH_SOURCE_TYPE_DATA_OR
#define DISPATCH_SOURCE_TYPE_MACH_RECV
#define DISPATCH_SOURCE_TYPE_MACH_SEND
#define DISPATCH_SOURCE_TYPE_PROC
#define DISPATCH_SOURCE_TYPE_READ
#define DISPATCH_SOURCE_TYPE_SIGNAL
#define DISPATCH_SOURCE_TYPE_TIMER
#define DISPATCH_SOURCE_TYPE_VNODE
#define DISPATCH_SOURCE_TYPE_WRITE
#define DISPATCH_SOURCE_TYPE_MEMORYPRESSURE
这里记录的是dispatch_source 中定时器timer的用法。
这里我就用dispatch_source 封装一个timer 的方法,可以在传入两个block,分别在循环执行,结束时执行。当然咯,下面这个方法还可以再加一个间隔时间参数。
- (void)startTimerWithTimeout:(NSTimeInterval)timeout eventBlock:(void (^)())eventBlock endBlock:(void (^)())endBlock
{
__block NSTimeInterval tempTimeout = timeout;
// 创建一个队列,不管你创建什么类型的队列,最终event_handler 都是在子线程中执行的
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 创建一个计时器类型的源
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置定时器参数,定时器开始的时间、每隔多久执行一次、精度(可以延迟的纳秒数,最高精度是0,实际还是会有偏差,感觉没什么卵用)
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"EVET ---- %@",[NSThread currentThread]);
if (tempTimeout <= 0) {
// 倒计时结束,取消源
dispatch_source_cancel(timer);
// 回到主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
// 倒计时结束时界面的UI的更新
if (endBlock) {
endBlock();
}
});
} else {
// 回到主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
// 做界面的UI的更新
if (eventBlock) {
eventBlock();
}
});
tempTimeout--;
}
});
dispatch_resume(timer);
}
封装好之后,调用起来就非常的Easy啦,并且看起来也挺舒服的。其实你可以把上面的方法封装成一个工具类方法。
[self startTimerWithTimeout:30 eventBlock:^{
NSLog(@"定时执行---%@",[NSThread currentThread]);
} endBlock:^{
NSLog(@"结束执行---%@",[NSThread currentThread]);
}];
15.Dispatch_source 中神奇的数据合并
上面介绍了dispatch_source 有多种类型,发现一种神奇的类型DISPATCH_SOURCE_TYPE_DATA_ADD,这种类型的source 有什么特别之处呢?假如我们并发执行多个任务,这种类型的source 会在任务完成时,将data 加1,然后如果主线程比较空闲,那么event_handler就会多次调用,而如果主线程恰好比较忙碌,那么就会将任务合并,event_handler调用次数就会比较少。
还是先上一个代码范例:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, queue);
dispatch_source_set_event_handler(source, ^{
unsigned long completion = dispatch_source_get_data(source);
NSLog(@"完成的任务个数:%lu----%@",completion,[NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"更新UI");
});
});
dispatch_resume(source);
dispatch_async(queue, ^{
NSLog(@"网络线程---%@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:2.0];
dispatch_source_merge_data(source, 1);
});
dispatch_async(queue, ^{
NSLog(@"网络线程---%@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:2.0];
dispatch_source_merge_data(source, 1);
});
//打印结果:
2016-07-13 15:38:45.911 PractiseProject[4130:150701] 网络线程---{number = 2, name = (null)}
2016-07-13 15:38:45.911 PractiseProject[4130:150694] 网络线程---{number = 3, name = (null)}
2016-07-13 15:38:47.915 PractiseProject[4130:150701] 完成的任务个数:2----{number = 2, name = (null)}
2016-07-13 15:38:47.915 PractiseProject[4130:150659] 更新UI
// 这也是打印结果:
2016-07-13 15:48:56.601 PractiseProject[4212:155405] 网络线程---{number = 2, name = (null)}
2016-07-13 15:48:56.601 PractiseProject[4212:155411] 网络线程---{number = 3, name = (null)}
2016-07-13 15:48:59.304 PractiseProject[4212:155405] 完成的任务个数:1----{number = 2, name = (null)}
2016-07-13 15:48:59.330 PractiseProject[4212:155377] 更新UI
2016-07-13 15:49:01.309 PractiseProject[4212:155415] 完成的任务个数:1----{number = 4, name = (null)}
2016-07-13 15:49:01.309 PractiseProject[4212:155377] 更新UI
它会根据主线程的繁忙与空闲,以及每个任务完成时的时间,减少返回次数或者每次返回。使用场景主要可以用在同时执行多个任务,任务的完成个数这种情况。
16.Queue-Specific
由于dispatch_get_current_queueAPI的移除,为了能够判断当前queue是否是之前创建的queue,我们可以利用dispatch_queue_set_specific和dispatch_get_specific给queue关联一个context data,后面再利用这个标识获取到context data。如果可以获取到说明当前上下文是在自己创建的queue中,如果不能获取到context data则表示当前是在其他队列上。使用场景: 自己创建一个队列,然后保证所有的操作都在该队列上执行。XMPP中有比较多的dispatch_queue_set_specific和dispatch_get_specific使用案例。
设置标识和关联的数据:
dispatch_queue_t queue = dispatch_queue_create("com.haley.cn", DISPATCH_QUEUE_SERIAL);
const void *queueSpecificKey = @"queueSpecificKey";
dispatch_queue_set_specific(queue, queueSpecificKey, &queueSpecificKey, NULL);
获取关联数据:
dispatch_get_specific(queueSpecificKey)
完整的示例:
dispatch_queue_t queue = dispatch_queue_create("com.haley.cn", DISPATCH_QUEUE_SERIAL);
// 当然这里也可以是其他类型的队列
// dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// dispatch_queue_t queue = dispatch_queue_create("com.haley.cn", DISPATCH_QUEUE_CONCURRENT);
const void *queueSpecificKey = @"queueSpecificKey";
dispatch_queue_set_specific(queue, queueSpecificKey, &queueSpecificKey, NULL);
dispatch_async(queue, ^{
NSLog(@"异步任务");
if (dispatch_get_specific(queueSpecificKey)) {
NSLog(@"com.haley.cn---1队列");
} else {
NSLog(@"---1其他队列");
}
});
NSLog(@"主线程,主队列");
if (dispatch_get_specific(queueSpecificKey)) {
NSLog(@"com.haley.cn---2队列");
} else {
NSLog(@"----2其他队列");
}
// 打印结果:
2016-07-11 14:30:56.772 PractiseProject[3379:152363] 主线程,主队列
2016-07-11 14:30:56.772 PractiseProject[3379:152363] ----2其他队列
2016-07-11 14:30:56.772 PractiseProject[3379:152451] 异步任务
2016-07-11 14:30:56.773 PractiseProject[3379:152451] com.haley.cn---1队列
dispatch_get_specific
所处的环境如果是在目标对列上时,就可以获取到关联的数据,否则就无法获取关联数据,返回NULL。看一看XMPP中的使用案例:
- (BOOL)activate:(XMPPStream *)aXmppStream{
__block BOOL result = YES;
dispatch_block_t block = ^{
if (xmppStream != nil) {
result = NO;
} else {
xmppStream = aXmppStream;
[xmppStream addDelegate:self delegateQueue:moduleQueue];
[xmppStream registerModule:self];
} };
if (dispatch_get_specific(moduleQueueTag)){
block();
} else {
dispatch_sync(moduleQueue, block);
return result;
}
为了保证block是在目标队列上执行,先判断当前是否在目标队列上(如果能取到关联数据,则说明在当前队列上),如果在目标队列上,直接执行block,否则就在目标队列上同步执行。
注意死锁的情况
情形一:在主线程中调度主队列完成一个同步任务,会发生死锁。
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"串行----线程:%@",[NSThread currentThread]);
});
}
如上代码,界面永远不会加载出来,里面的NSLog永远也不会执行。原因是ViewDidLoad是在主队列的主线程中执行,执行到dispatch_sync 时会阻塞住,等待dispatch_sync中的打印任务执行完毕。而dispatch_sync又会等viewDidLoad执行完毕,再开始执行,因此就互相等待发生死锁。
情形二:在串行队列的同步任务中再执行同步任务,会发生死锁。
dispatch_queue_t serial_queue = dispatch_queue_create("com.haley.com", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serial_queue, ^{
NSLog(@"串行1----线程:%@",[NSThread currentThread]);
dispatch_sync(serial_queue, ^{
NSLog(@"串行2----线程:%@",[NSThread currentThread]);
});
});
上面示例中的NSLog(@"串行1----线程:%@",[NSThread currentThread]);会打印.
但是NSLog(@"串行2----线程:%@",[NSThread currentThread]);永远也不会执行。
因为串行队列一次只能执行一个任务,执行完毕返回后,才会执行下一个任务,而外层任务的完成需要等待内层任务的结束,而内层任务的开始需要等外层任务结束。
其实情形一是情形二的一种特殊情况。
情形三:在串行队列的异步任务中再嵌套执行同步任务,也会发生死锁。
dispatch_queue_t serial_queue = dispatch_queue_create("com.haley.com", DISPATCH_QUEUE_SERIAL);
dispatch_async(serial_queue, ^{
NSLog(@"串行异步----线程:%@",[NSThread currentThread]);
dispatch_sync(serial_queue, ^{
NSLog(@"串行2----线程:%@",[NSThread currentThread]);
});
[NSThread sleepForTimeInterval:2.0];
});
同样的,由于串行队列一次只能执行一个任务,任务结束后,才能执行下一个任务。
所以异步任务的结束需要等里面同步任务结束,而里面同步任务的开始需要等外面异步任务结束,所以就相互等待,发生死锁了。