[iOS]GCD(一)
文章目录
- [iOS]GCD(一)
- GCD的概要
- GCD的API
- Dispatch Queue
- dispatch_queue_create
- Main Dispatch Queue和 Global Dispatch Queue.
- Main Dispatch_set_target_queue
- dispatch_after
- Dispatch Group
- dispatch_barrier_async
- dispatch_apply
- dispatch_apply
- dispatch_suspend/dispatch_resume
- Dispatch Semaphore
- dispatch_once
- Dispatch I/O
- GCD的实现
- Dispatch Queue
- Main Dispatch Queue 在RunLoop 中执行Block
- Global Main Dispatch Queue 有如下8种
- XNU内核持有4种workqueue
- Dispatch Source
GCD的概要
Grand Central Dispatch (GCD)是异步执行任务的技术之一。一般将应用程序中记述的线程 管理用的代码在系统级中实现。开发者需要定义想执行的任务并追加到适当的 Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的 一部分来实现的, 因此可统一管理,也可执行任务,这样就比以前的线程更有效率
OSX和iOS的核心XNU内核在发生操作系统事件时 (如每隔一定时间,唤起系统调用等 情况)会切换执行路径。执行中路径的状态,例如CPU 的寄存器等信息保存到各自路径专用的内存块中,从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令列。这被称为 “上下文切换”。
由于使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上 去就好像1 个CPU核能够并列地执行多个线程一样。而且在具有多个CPU核的情况下,就不是 “看上去像” 了,而是真的提供了多个CPU核并行执行多个线程的技术。 这种利用多线程编程的技术就被称为 “多线程编程”。
但是多线程编程实际上是一种易发生各种问题的编程技术。比如多个线程更新相同的资源 会导致数据的不一致 (数据竞争)、停止等待事件的线程会导致多个线程相互持续等待 (死锁)、使用太多线程会消耗大量内存等 。
GCD的API
Dispatch Queue
开发者要做的只是定义想执行的任务井追加到适当的Dispatch Queue 中
该源代码使用Block语法“定义想执行的任务”,通过dispatch_async函数“追加” 赋值在变量 queue 的 “ Dispatch Queue ”。仅这样就可使指定的Block在线程中执行
“DispatchQueue” 是什么呢?如其名称所示,是执行处理的等待队列。应用程序编程人 员通过dispat ch_async函数等API,在Block 语法中记述想执行的处理并将其追加到Dispatch Queue中。DispatchQueue按照追加的顺序(先进先出FIFO, First-In-First-Out)执行处理
dispatch_async (queue, ^{
//想执行的任务
}) :
通过Dispatch Queue 执行处理 另外在执行处理时存在两种DispatchQueue,一种是等待现在执行中处理的Serial Dispatch Queue,另一种是不等待现在执行中处理的Concurrent DispatchQueue
当变量queue 为Serial Dispatch Queue
时,因为要等待现在执行中的处理结束,所以首先执行blk0,blk0执行结束后,接着执行blkl ,blk1 结束后再开始执行blk2,如此重复。同时执行 的处理数只能有1 个。即执行该源代码后,一定按照0123顺序进行处理。
当变量queue 为Concurrent Dispatch Queue
时,因为不用等待现在执行中的处理结束,所以 首先执行bIkO,不管bIkO的执行是否结束,都开始执行后面的bIkl ,不管blk1的执行是否结束, 都开始执行后面的blk2,如此重复循环。 这样虽然不用等待处理结束,可以并行执行多个处理,但并行执行的处理数量取决于当前系统的状态。即iOS 和OS X 基 于Dispatch Queue 中的处理数、CPU 核数以及CPU 负荷等当前系统的状态来决定Concurrtent Dispatch Queue 中并行执行的处理数。所谓“ 并行执行”,就是使用 多个线程同时执行多个处理
iOS 和OSX的核心——XNU内核决定应当使用的线程数,并只生成所需的线程执行处理。 另外,当处理结束,应当执行的处理数减少时,XNU内核会结束不再需要的线程。XNU 内核仅 使用Concurrent Dispatch Queue便可完美地管理并行执行多个处理的线程。
在Concurrent DispatchQueue中执行处理时,执行顺序会根据处理内容和系统状态发 生改变。它不同于执行顺序固定的Serial Dispatch Queue。在不能改变执行的处理顺序或不想并行执行多处理时使用 Serial Dispatch Queue
dispatch_queue_create
通过dispatch _queue_create 函数可生成DispatchQueue。以下源代码生成了Serial Dispatch Queue.
dispatch_queue_t mySerialDispatchQueue =
dispatch_queue_create ("com. example.gcd.MySerialDispatchQueue", NULL) :
在说明dispatch_queue_create 函数之前,先讲一下关于Serial DispatchQueue 生成个数的注意事项。 如前所述,Concurrent Dispatch Queue 并行执行多个追加处理,而Serial Dispatch Queue 同时只能执行 1 个追加处理。虽然Serial Dispatch Queue 和Concurrent Dispatch Queue 受到系统资源的限制,但 用 dispatch_queue_create函数可生成任意多个 dispatch_queue_create 。
当生成多个Serial Dispatch Queue 时,各个Serial Dispatch Queue 将 并行执行。虽然在1 个Serial Dispatch Queue 中同时只能执行一个追加处理,但如果将处理分别追加到4 个Serial Dispatch Queue 中,各个Serial Dispatch Queue 执行1 个,即为同时执行4 个处理
像之前列举的多线程编程问题一样,如果过多使用多线程,就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能
只在为了避免多线程编程问题之一多个线程更新相同资源导致数据竞争时使用Serial Dispatch Queue.
但是Serial DispatchQueue的生成个数应当仅限所必需的数量
当想并行执行不发生数据竞争等问题的处理时,使用Concurrent Dispatch Queue。而且对于 Concurrent Dispatch Queue 来说,不管生成多少,由 于XNU 内核只使用有效管理的线程,因此不 会发生Serial Dispatch Queue 的那些问题
下面我们回来继续讲dispatch_queue_create函数
该函数的第一个参数指定Serial Dispatch Queue 的名称
Dispatch Queue 的名称推荐使用应用程序ID这种逆序全程域名 (FQDN, fullyqualifieddomainname)
该名称在Xcode和Instruments的调试器中作为Dispatch Queue 名称表示
生成Serial DispatchQueue
时,像该源代码这样,将第二个参数指定为NULL
生成ConcurrentDispatchQueue
时,像下面源代码 一样 ,指定为DISPATCH_QUEUE_CONCURRENT
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com. example.gcd.MyConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
dispatchLqueue_create函数的返回值为表示DispatchQueue的“dispatch _queue_t类型”。在之
前源代码中所出现的变量queue均为dispatch_queue_t 类型变量
dispatch_queue_t myconcurrentDispatchQueue = dispatch_queue_create ("com. example.gcd.MyConcurrentDispatchQueue", DISPATCH_ QUEUE_CONCURRENT);
dispatch_async (myConcurrentDispatchQueue, ^{NSLog (@"block on myConcurrentDispatchQueue");
});
该源代码在Concurrent DispatchQueue 中执行指定的Block
Main Dispatch Queue和 Global Dispatch Queue.
是获取系统标准提供的DispatchQueue。
实际上,不用特意生成Dispatch Queue 系统也会给我们提供几个
那就是Main Dispatch Queue和 Global Dispatch Queue.
MainDispatchQueue正如其名称中含有的“Main” 一样,是在主线程中执行的Dispatch Queue。 因为主线程只有 1 个 , 所以 MainDispatchQueue 自然就是 SerialDispatchQueue 。
追加到 MainDispatchQueue 的处理在主线程的 RunLoop中执行。由于在主线程中执行,因此要将用户界面的界面更新等 一些必须在主线程中执行的处理追加到MainDispatchQueue使用。 这正好与NSObject 类的performSelectorOnMainThread实例方法这一执行方法相同
另一个 Global Dispatch Queue 是所有程序都能够使用的 ConcurrentDispatchQueue
没 有必要通过dispatch _queue _create 函数逐个生成Concurrent Dispatch Queue。只要获取Global Dispatch Queue 使用即可。
另外,Global Dispat ch Queue 有4 个执行优先级,分别是高优先级 (High Priorit y)、默认优 先级 (Default Priority)、低优先级 (LowPriority)和后台优先级 (Background Priority)
通过 XNU内核管理的用于Global Dispatch Queue 的线程,将各自使用的Global Dispatch Queue 的执 行优先级作为线程的执行优先级使用。在向Global Dispatch Queue追加处理时,应选择与处理内 容对应的执行优先级的Global DispatchQueue。
但是通过XNU内核用于Global Dispatch Queue 的线程并不能保证实时性,因此执行优先级 只是大致的判断。例如在处理内容的执行可有可无时,使用后台优先级的Global Dispatch Queue 等,只能进行这种程度的区分。
各 Dispatch Queue 的获取方法如下
/** Main Dispatch Queue 的获取方法 */
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
/** Global DispatchQueue(高优先级)的获取方法*/
dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY HIGH, 0);
其他优先级按表了类比 就不一一举例了
另外,对于MainDispatchQueue和Global DispatchQueue执行dispatch_retain函数和dispatch_ release 函数不会引起任何变化,也不会有任何问题。这也是获取并使用Global Dispat ch Queue 比生成、使用、释放Concurrent Dispatch Queue 更轻松的原因
Main Dispatch_set_target_queue
dispatch_queue_create函数生成的DispatchQueue 不管是SerialDispatchQueue还是Concurrent Dispat ch Queue,都使用与默认优先级Global Dispatch Queue 相同执行优先级的线程。而变更生成的Dispatch Queue的执行优先级要使用dispatch_set_target_queue
函数
dispatch_queue_t mySerialDispatchQueue =
dispatch_queue_create("com. example.gcd.MySerialDispatchQueue", NULL); dispatch_queue_t globalDispatchQueueBackground =
dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); dispatch_set_target_queue (mySerialDispatchQueue, globalDispatchQueueBackground) ;
dispatch_set_target_queue (mySerialDispatchQueue, globalDispatchQueueBackground) :
指定要变更执行优先级的Dispatch Queue 为dispatch_set_target_queue 函数的第一个参数
指定与要使用的执行优先级相同优先级的GlobalDispatchQueue为第二个参数(目标)
第 一个参数如果指定系统提供的Main Dispatch Queue 和Global Dispatch Queue 则不知道会出现什么状况, 因此这些均不可指定。
将Dispatch Queue 指定为 dispatch_set_target_queue函数的参数,不仅可以变更Dispatch Queue 的执行优先级,还可以作成DispatchQueue的执行阶层。如果在多个SerialDispatchQueue中用 dispatch_set_target_queue函数指定目标为某一个Serial DispatchQueue,那么原先本应并行执行的多个SerialDispatchQueue,在目标SerialDispatchQueue 上只能同时执行一个处理
在必须将不可并行执行的处理追加到多个Serial Dispatch Queue 中时,如果使用dispatch_ set_target _queue函数将目标指定为某一个SerialDispatchQueue,即可防止处理并行执行
dispatch_after
经常会有这样的情况:想在3秒后执行处理。可能不仅限于3 秒,总之,这种想在指定时间 后执行处理的情况,可使用dispatch_after
函数来实现
dispatch_time_t time = dispatch_time (DISPATCH_TIME_ NOW, 3ull * NSEC_PER_SEC);
dispatch_after (time, dispatch_get_main_queue(), ^{NSLog (@"waited at least three seconds.");
});
第一个参数是指定时间用的dispatch_time_t 类型的值。该值使用dispatch_time函数或 dispatch_walltime 函数作成
第二个参数指定要追加处理的Dispatch Queue
第三个参数指定记述要执行处理的Block
需要注意的是,dispatch_after 函数并不是在指定时间后执行处理,而只是 在指定时间追加处理到Dispatch Queue
此源代码与在3 秒后用dispatch_async 函数追加Block 到Main Dispatch Queue 的相同
dispatch_time函数能够获取从第一个参数dispatch_time_t 类型值中指定的时间开始,到第二个参数指定的毫微秒单位时间后的时间。第一个参数经常使用的值是之前源代码中出现的DISPATCH_TIME_NOW。这表示现在的时间
数值和NSEC_PER_SEC的乘积得到单位为毫微秒的数值。“ul” 是C语言的数值字面量, 是 显 式 表 明 类 型 时 使 用 的 字 符 串 ( 表 示 “ u n s i g n e d l o n g l o n g ” )。 如 果 使 用 N S E C _ P E R _ M S E C 则 可 以 以 毫 秒 为 单 位 计 算 。 以 下源 代 码 获 取 表 示 从 现 在 开 始 1 5 0 毫 秒 后 时 间 的 值
dispatch_walltime
函数由POSIX中使用的struct timespec 类型的时间得到dispatch_time_t 类型的值。dispatch_time函数通常用于计算相对时间,而dispatch_walltime 函数用于计算绝对时间。 例如在dispatchLafter 函数中想指定2011年11月11日11时11分11秒这一绝对时间的情况,这 可作为粗略的闹钟功能使用。
struct timesp ec类型的时间可以很轻松地通过NSDate 类对象作成
dispatch_time_t getDispatchTimeByDate (NSDate *date)NSTimeInterval interval;double second, subsecond;struct timespec time;dispatch_time_t milestone;interval = [date timeIntervalSince1970];subsecond = modf (interval, &second) ;time.tv_sec = second;time.tv_nsec = subsecond * NSEC. _PER_ SEC;milestone = dispatch_walltime (&time, 0);
return milestone;
}
Dispatch Group
使用Dispatch Group
追加3个Block 到GlobalDispatchQueue,这些Block 如果全部执行完毕,就会执行MainDispatchQueue中结束处理用的Block。
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, ^{NSLog (@"blk0");});dispatch_group_async(group, queue, ^{NSLog (@"blk1");});dispatch_group_async(group, queue, ^{NSLog ( @"blk2");});dispatch_group_notify(group, dispatch_get_main_queue (), ^{NSLog (@"done");});
结果如下
因为向 GlobalDispatchQueue 即 ConcurrentDispatchQueue 追加处理,多个线程并行执行,所以追加处理的执行顺序不定。执行时会发生变化,但是此执行结果的done一定是最后输出的
dispatch_group_
async 函数与dispatch_async函数相同,都追加Block到指定的Dispatch Queue 中。与dispatch_async 函数不同的是指定生成的Dispatch Group 为第一个参数。指定的Block属于指定的DispatchGroup
在追加到DispatchGroup中的处理全部执行结束时,该源代码中使用的dispatch_group_notify 函数会将执行的Block 追加到Dispatch Queue 中
将第一个参数指定为要监视的Dispatch Group。 在追加到该Dispatch Group 的全部处理执行结束时
将第三个参数的Block 追加到第二个参数 的Dispatch Queue 中
在dispatch_group _notify 函数中不管指定什么样的Dispatch Queue,属于 Dispatch Group 的全部处理在追加指定的Block 时都已执行结束。
另外,在DispatchGroup中也可以使用dispatch group _wait函数仅等待全部处理执行结束。
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, ^{NSLog(@"b1k0");
});
dispatch_group_async(group, queue, ^{NSLog (@"blk1");
});
dispatch_group_async(group, queue, ^{NSLog (@"b1k2");
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch group_wait 函数的第二个参数指定为等待的时间(超时)。它属于dispaich_time_ t 类型的值。该源代码使用DISPATCH _TIME_FOREVER,意味着永久等待。只要属于DispatchGroup 的处理尚未执行结束,就会一直等待,中途不能取消。
如果dispatch group_wait 函数的返回值不为0,就意味着虽然经过了指定的时间,但属于 Dispatch Group 的某 一个处理还在执行中。如果返回值为0,那么全部处理执行结束。当等待 时间为DISPATCH_TIME,_FOREVER、由dispatch group _wait 函数返回时,由于属于Dispatch Group 的处理必定全部执行结束,因此返回值恒为0。
指定DISPATCH_TIME_NOW,则不用任何等待即可判定属于Dispatch Gr oup 的处理是否执 行结束
在主线程的RunLoop 的每次循环中,可检查执行是否结束,从而不耗费多余的等待时间, 虽然这样也可以,但一般在这种情形下,还是推荐用dispatch_group_notify函数追加结束处理到 Main Dispatch Queue 中。这是因为dispatch_group_notify 函数可以简化源代码
dispatch_barrier_async
复习一下读写锁是啥
读写锁(Readers-Writer Lock)顾名思义是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的。总结来说,读写锁的特点是:读读不互斥、读写互斥、写写互斥。它适用于多读的业务场景,使用它可以有效的提高程序的执行性能,也能避免读取到操作了一半的临时数据。
前面讲到Concurrent Dispatch Queue可以并行执行操作,显然符合我们读锁的要求,能够高效率地访问数据
而Serial Dispatch Queue显然适合用于在没有读取操作时进行写入操作
这么看单纯使用Serial Dispatch Queue来实现读写操作显然不如二者结合来的效率更快
那么我们要怎么做到这一点呢?
可以使用Dispatch Group和dispatch_set_target_queue实现,但是代码复杂
也有更聪明的方法,那就是dispatch+barrier_async函数
如果只有单纯的读取操作
dispatch_queue_t queue = dispatch_queue_create (
"com. example.gcd. ForBarrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async (queue, blk0_for_reading);
dispatch_async (queue, bik1_for_reading);
dispatch_async (queue, bik2_for_reading);
dispatch_async (queue, b1k3 for_reading);
dispatch_async (queue, bik4_for_reading);
dispatch_async (queue, bik5 for_reading);
dispatch_async (queue, bik6_for_reading);
dispatch_async (queue, bik7_for_reading);
dispatch_release (queue) ;
在中间加入写入操作时
如果直接加入
NSInteger __block data = 666;dispatch_queue_t queue = dispatch_queue_create ("com. example.gcd. ForBarrier", DISPATCH_QUEUE_CONCURRENT);dispatch_async (queue, ^{NSLog(@"blk0_for_reading get data:%ld", data);});dispatch_async (queue, ^{NSLog(@"blk1_for_reading get data:%ld", data);});dispatch_async (queue, ^{NSLog(@"blk2_for_reading get data:%ld", data);});dispatch_async (queue, ^{NSLog(@"blk3_for_reading get data:%ld", data);});dispatch_async (queue, ^{NSLog(@"blk4_for_reading get data:%ld", data);});dispatch_async (queue, ^{data = 999;NSLog(@"data has changed");});dispatch_async (queue, ^{NSLog(@"blk5_for_reading get data:%ld", data);});dispatch_async (queue, ^{NSLog(@"blk6_for_reading get data:%ld", data);});dispatch_async (queue, ^{NSLog(@"blk7_for_reading get data:%ld", data);});
我们希望的结果是读取结果是在blk5之前读取到的data为666 其余为999
但是结果是这样的
简单地在dispatch_async函数中加入写入处理,那么根据Concurrent Dispatch Queue 的性质,就有可能在追加到写入处理前面的处理中读取到与期待不符的数据,还可能因非法访问导致应用程序异常结束。如果追加多个写入处理,则可能发生更多问题,比如数据竞争等
因此我们要使用dispatch_barrier_async
函数。dispatch_barrier_async函数会等待追加到 Concurrent Dispatch Queue 上的并行执行的处理全部结束之后,再将指定的处理追加到该 Concurrent Dispatch Queue中。然后在由dispatch_barrier_async函数追加的处理执行完毕后, Concurrent Dispatch Queue 才恢复为一般的动作,追加到该 Concurrent Dispatch Queue 的处理又开始并行执行。
NSInteger __block data = 666;dispatch_queue_t queue = dispatch_queue_create ("com. example.gcd. ForBarrier", DISPATCH_QUEUE_CONCURRENT);dispatch_async (queue, ^{NSLog(@"blk0_for_reading get data:%ld", data);});dispatch_async (queue, ^{NSLog(@"blk1_for_reading get data:%ld", data);});dispatch_async (queue, ^{NSLog(@"blk2_for_reading get data:%ld", data);});dispatch_async (queue, ^{NSLog(@"blk3_for_reading get data:%ld", data);});dispatch_async (queue, ^{NSLog(@"blk4_for_reading get data:%ld", data);});dispatch_barrier_async (queue, ^{data = 999;NSLog(@"data has changed");});dispatch_async (queue, ^{NSLog(@"blk5_for_reading get data:%ld", data);});dispatch_async (queue, ^{NSLog(@"blk6_for_reading get data:%ld", data);});dispatch_async (queue, ^{NSLog(@"blk7_for_reading get data:%ld", data);});
结果如下
咱们的dispatch_barrier_async
函数到底干了啥?一张图告诉你
不难看出,使用Concurrent DispatchQueue和dispatch_barrier_async函数可实现高效率的数据库访问和文件访问
哦对了 加个TIPS
从iOS 6和OS X 10.8开始,GCD(Grand Central Dispatch)引入了ARC(Automatic Reference Counting)来管理内存,这意味着你不需要手动调用 dispatch_release 来释放队列。ARC会自动处理内存管理,包括对 GCD 中的对象的管理。因此,调用 dispatch_release(queue) 是多余且无效的,反而可能导致错误。
如果你在使用 ARC 环境下编写代码,并且需要创建和管理 Dispatch Queue,只需简单地调用 dispatch_queue_create 来创建队列,然后将其用于任务调度即可。记住不要在 ARC 下手动释放 GCD 的对象,因为 ARC 会自动处理内存管理。
dispatch_apply
dispatch_async 函 数 的 “ async ” 意 味 着 “ 非 同 步 ” (asynchronous),就是将指定的Block“非同步” 地追加到指定的DispatchQueue中。dispatch_async 函数不做任何等待
既然有“async”,当然也就有“sync”,即dispatch_sync 函数。它意味着“ 同步” (synchronous),也就是将指定的Block“同步”追加到指定的 Dispatch Queue中。在追加Block 结束之前,dispatch_sync 函数会一直等待
如dispatch_group _wait 函数说明所示,“ 等待” 意味着当前线程停止。
一旦调用dispatch_sync函数,那么在指定的处理执行结束之前,该函数不会返回。dispatch_ sync 函数可简化源代码,也可说是简易版的dispatch_group_wait 函数
看看下面这个运用了dispatch_sync
函数的简单代码
dispatch_queue_t queue = dispatch_get_global_queue (DISPATCH_QUEUE_ PRIORITY_DEFAULT, 0);
dispatch_sync (queue , ^{/ * 处理 * /});
执行MainDispatchQueue时,使用另外的线程GlobalDispatch Queue 进行处理
在另外的线程GlobalDispatch Queue 处理结束后dispatch_sync再使用所得到的结果进行处理
因为dispatch_sync函数使用简单,所以也容易引起问题,即死锁
不知道大家还记不记得自动引用计数部分提到的保留环?
保留环(Retain Cycle)是指两个或多个对象在彼此之间互相持有对方的强引用,导致它们无法被释放,从而造成内存泄漏的情况。当两个对象互相持有对方的强引用时,它们的引用计数永远不会变为零,因此系统也无法释放它们所占用的内存空间。
保留环里也有自己持有自己强引用的状态
这里的死锁可以类比理解
比如下面这个代码
sync在等待主线程结束再执行自己的的操作
听起来很合理对不对?
那么主线程在干嘛,主线程在追加sync的操作
也是保留环中属于互相持有,释放不掉了
与Main Dispatch Queue共用出现死锁
与Serial Dispatch Queue共用出现死锁
由dispatch_barrier_async函数中含有async可推测出,相应的也有dispatch_barrier_ sync函数。dispatch_barrier_async函数的作用是在等待追加的处理全部执行结束后,再追加处理到DispatchQueue中,此外 ,它还与dispatch_sync函数相同,会等待追加处理的执行结束。
dispatch_apply
dispatch_apply 函数是 dispatch_sync 函 数 和 DispatchGroup 的关联API。该函数按指定的次数将指定的Block 追加到指定的Dispatch Queue 中,并等待全部处理执行结束
来一段代码
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {NSLog (@"%zu", index);
});
NSLog (@"done");
结果为
因为在Global Dispatch Queue 中执行处理,所以各个处理的执行时间不定。但是输出结果中 最后的done必定在最后的位置上。这是因为dispatch_apply
函数会等待全部处理执行结束
那么这个函数那么多参数,到底传什么?怎么用?
第一个参数为重复次数
第二个参数为追加对象的Dispatch Queue
第三个参数为追加的处理
与到目前为止所出现的例子不同,第三个参数的Block 为带有参数的Block。这是为了按第一 个参数重复追加Block 并区分各个Block 而使用。例如要对NSArray 类对象的所有元素执行处 理时,不必一个一个编写for循环部分,而是可以这样、
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {t index) {NSLog (@"%zu: %@", index, [array objectAtIndex:index]) ;
});
NSLog (@"done");
这样可简单地在Global DispatchQueue 中对所有元素执行Block
另外,由于dispatch_apply函数也与dispatch_sync函数相同,会等待处理执行结束,因此推荐 在 dispatch_async 函数中非同步地执行 dispatch_apply 函数
dispatch_suspend/dispatch_resume
当追加大量处理到Dispatch Queue 时,在追加处理的过程中,有时希望不执行已追加的处理 。例如演算结果被Block截获时, 一些处理会对这个演算结果造成影响。在这种情况下, 只要挂起 Dispatch Queue 即可,当可以执行时再恢复
dispatch_suspend
函数挂起指定的Dispatch Queue
dispatch_suspend (queue) ;
dispatch_resume
函数恢复指定的Dispatch Queue
dispatch_resume (queue) ;
这些函数对已经执行的处理没有影响
挂起后,追加到Dispatch Queue中但尚未执行的处理在此之后停止执行
而恢复则使得这些处理能够继续执行。
Dispatch Semaphore
如前所述,当并行执行的处理更新数据时,会产生数据不一致的情况,有时应用程序还会异 常结束。虽然使用Serial Dispatch Queue和dispatch_barrier_async函数可避免这类问题,但有必要进行更细粒度的排他控制
dispatch_queue_t queue = dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [[NSMutableArray alloc]init];
for (int i = 0; i < 100000; ++i) {dispatch_async (queue, ^{[array addObject: [NSNumber numberWithInt:i]];});
}
因为该源代码使用Global Dispatch Queue 更新NSMutableArray 类对象,所以执行后由内存错误导致应用程序异常结束的概率很高。此时应使用Dispatch Semaphore
Dispatch Semaphore 是持有计数的信号,该计数是多线程编程中的计数类型信号。所谓信号,类似于过马路时常用的手旗。可以通过时举起手旗,不可通过时放下手旗。而在 Dispatch Semaphore 中,使用计数来实现该功能。计数为0 时等待,计数为1 或大于1 时,减去1 而不等待
通过dispatch_ semaphore_create
函数生成DispatchSemaphore
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
参数表示计数的初始值。本例将计数值初始化为“1”
dispatch_semaphore_wait (semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait 函数等待 DispatchSemaphore的计数值达到大或等于1。当计数值大于等于1,或者在待机中计数值大于等于1时,对该计数进行减法并从dispatch_semaphore_wait 函数返回。第二个参数与dispatch_group_wait 函数等相同,由dispatch_time_t 类型值指定等待时间。该例的参数意味着永久等待。dispatch_semaphore_wait 函数的返回值也与 dispatch_group _wait 函数相同
直接上个代码
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);/** 生成DispatchSemaphore* DispatchSemaphore 的计数初始值设定为“ 1 ”* 保证可访问NSMutableArray类对象的线程* 同时只能有1个*/dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);NSMutableArray *array = [[NSMutableArray alloc] init];for(int i = 0; 1 < 100000; ++i) {dispatch_async (queue, ^{/** 等待Dispatch Semaphorea* 一直等待到DispatchSemaphore的计数值达到大于等于1*/dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);/** 由于Dispatch Semaphore的计数值达到大于等于1 所以将Dispatch Semaphore的计数值减去1,* dispatch_semaphore_wait 函数执行返回。* 即执行到此时的* DispatchSemaphore 的计数值恒为“ 0 ”* 由于可访问NSMutableArray 类对象的线程只有 1 个* 因此可安全地进行更新。*/[array addObject: [NSNumber numberWithInt:i]];/** 排他控制处理结束,* 所以通过 dispatch_semaphore_signal 函数将Dispatch Semaphore的计数值加1* 如果有通过dispatch _semaphore_wait 函数* 等待Dispatch Semaphore的计数值增加的线程* 就由最先等待的线程执行。*/dispatch_semaphore_signal(semaphore);});}
在没有Serial Dispatch Queue 和dispatch_barrier_async 函数那么大粒度且一部分处理需要进 行排他控制的情况下,Dispatch Semaphore 便可发挥威力
dispatch_once
dispatch _once函数是保证在应用程序执行中只执行一次指定处理的API
下面这种经常出现 的用来进行初始化的源代码可通过dispatch _once
函数简化
static int initialized = NO;
if (initialized = NO) {/** 初始化*/
initialized = YES;
}
如果使用dispatch _once函数,则源代码写为:
static dispatch_once_t pred;
dispatch_once (&pred, ^ (/** 初始化*/
});
源代码看起来没有太大的变化。但是通过dispatch _once函数,该源代码即使在多线程环境 下执行,也可保证百分之百安全,然后这也是学单例时讲过的老朋友了,不多赘述
Dispatch I/O
大家可能想过,在读取较大文件时,如果将文件分成合适的大小并使用Global Dispatch Queue 并列读取的话,应该会比一般的读取速度快不少。现今的输入/ 输出硬件已经可以做到一 次使用多个线程更快地并列读取了。能实现这一功能的就是Dispatch I/O 和Dispatch Data。
通过Dispatch
I/O读写文件时,使用GlobalDispatchQueue 将1 个文件按某个大小read/write
dispatch_async (queue, ^{/*读取0~8191字节*/});
dispatch_async (queue, ^{/*读取8192~16383字节*/});
dispatch_async (queue, ^(/*速取16384~24575字节*/});
dispatch_async (queue, ^(/*读取24576~32767字节*/});
dispatch_async (queue, ^(/*读取32768~40959字节*/});
可像上面这样,将文件分割为一块一块地进行读取处理。分割读取的数据通过使用Dispatch Data 可更为简单地进行结合和分割
如果想提高文件读取速度,可以尝试使用Dispat ch I/ O
GCD的实现
Dispatch Queue
通常,应用程序中编写的线程管理用的代码要在系统级实现。
实际上正如这句话所说,在系统级即iOS 和OSX的核心XNU内核级上实现。 因此,无论编程人员如何努力编写管理线程的代码,在性能方面也不可能胜过XNU内核级所实现的 GCD
使用GCD要比使用pthreads 和NSThread 这些 一般的多线程编程API更好
并且使用GCD 就不必编写为操作线程反复出现的类似的源代码 (这被称为固定源代码片断),而可以在线程中集中实现处理内容
编程人员所使用GCD的API 全部为包含在libdispatch库中的C语言函数。DispatchQueue 通过 结构体和链表,被实现为FIFO队列。
FIFO队列管理是通过dispatch_async等函数所追加的Block
Block并不是直接加入FIFO队列,而是先加入DispatchContinuation这一dispatch_continuation_t
类型结构体中,然后再加入FIFO队列。该Dispatch Continuation 用于记忆Block 所属的Dispatch Group 和其他一些信息,相当于一般常说的执行上下文
DispatchQueue可通过dispatch_set_target _queue
函数设定,可以设定执行该DispatchQueue 处理的DispatchQueue 为目标。该目标可像串珠子一样,设定多个连接在一起的DispatchQueue。
但是在连接串的最后必须设定为Main Dispatch Queue,或各种优先级的Global Dispatch Queue,
或是准备用于Serial DispatchQueue 的各种优先级的Global DispatchQueue
Main Dispatch Queue 在RunLoop 中执行Block
Global Main Dispatch Queue 有如下8种
• Global Dispatch Queue (High Priority)
• Global Dispatch Queue (Default Priority)
• Global Dispatch Queue (Low Priority)
• Global Dispatch Queue (Background Priority)
• Global Dispatch Queue (High Overcommit Priority)
• Global Dispatch Queue (Default Overcommit Priority)
• Global Dispatch Queue (Low Overcommit Priority)
• Global Dispatch Queue (Background Overcommit Priority)
优先级中附有Overcommit 的Global Dispatch Queue 使用在Serial Dispatch Queue 中。如 Overcommit 这个名称所示,不管系统状态如何,都会强制生成线程的DispatchQueue
这8种GlobalDispatchQueue各使用1个pthrcead_workqueue 。GCD初始化时,使用pthread_ work queue_create_np 函数生成pthread_workqueue。
pthread
Lworkqueue包含在Libc提供的pthreadsAPI中。其使用bsdthread_register fil workq_open 系统调用,在初始化XNU内核的workqueue之后获取workqueue信息。
XNU内核持有4种workqueue
• WORKQUEUE_HGIH_PRIOQUEUE
• WORKQUEUE_DEFAULT_PRIOQUEUE
• WORKQUEUE_LOW_PRIOQUEUE
• WORKQUEUE_BG_PRIOQUEUE
以上为4 种执行优先级的workqueue。该执行优先级与Global Dispatch Queue 的4种执行优先级相同
先级相同。
下面看一下Dispatch Queue 中执行 Block 的过程
当在 Global Dispatch Queue 中执行 Block 时 , 从 Global Dispatch Queue 自身的 FIFO 队列中取出 DispatchContinuation,调用 pthread_workqueue_additem_np 函数。将该Global Dispatch Queue 自身、符合其优先级的workqueue 信息 以及为执行DispatchContinuation 的回调函数等传递给参数
pthread_workqueue_additem_np 函数使用workq_kemretur 系统调用,通知workqueue 增加应 当执行的项目。
根据该通知,XNU 内核基于系统状态判断是否要生成线程。
如果是Overcommit 优先级的GlobalDispatchQueue, workqueue则始终生成线程。
该线程虽然与iOS和OSX中通常使用的线程大致相同,但是有一部分pthreadAPI 不能使用。
另外,因为workqueue 生成的线程在实现用 于workqueue 的线程计划表中运行,所以与 一般 线程的上下文切换不同。这里也隐藏着使用GCD的原因。
workqueue 的线程执行pthread_workqueue 函数,该函数调用libdispatch 的回调函数。在该回 调函数中执行加入到DispatchContinuation 的Block。
Block 执行结束后,进行通知Dispatch Group 结束、释放Dispatch Continuation 等处理,开始 准备执行加入到Global DispatchQueue中的下一个Block。
以上就是Dispatch Queue 执行的大概过程
Dispatch Source
GCD中除了主要的DispatchQueue外,还有不太引人注目的Dispatch Source。它是BSD系内核惯有功能kqueue的包装。
事件发生时,在指定的DispatchQueue 中可执行事件的处理。 下面我们使用DISPATCH_SOURCE _TYPE _READ,异步读取文件映像。
//不贴代码了
//书上这一段一直报错
//and我也没看懂这个部分
实际上 Dispatch Queue 没有“取消”这 一概 念 。 一旦将处理追加到 Dispatch Queue 中,就 没有方法可将该处理去除,也没有方法可在执行中取消该处理。编程人员要么在处理中导入取消这一概念, 要么放弃取消,或者使用NSOperationQueue 等其他方法。
Dispatch Source 与Dispatch Queue 不同,是可以取消的。而且取消时必须执行的处理可指定 为回调用的Block 形式。因此使用Dispatch Source 实现XNU 内核中发生的事件处理要比直接使用 kqueue 实现更为简单。