[OC学习笔记]Grand Central Dispatch

一、GCD概要

(一)什么是GCD

什么是GCD?以下摘自苹果的官方说明。
Grand Central Dispatch(GCD)是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,这样就比以前的线程更有效率。
也就是说,GCD用我们难以置信的非常简洁的记述方法,实现了极为复杂繁琐的多线程编程,可以说这是一项划时代的技术。下面是使用了GCD源代码的例子,虽然稍显抽象,但从中也能感受到GCD的威力。

dispatch async(queue, ^{
    
    
	/*
	 *长时间处理
	 *例如AR用画像识别*例如数据库访问
	 */
	/*
	 *长时间处理结束, 主线程使用该处理结果。 
	 */
	dispatch_async(dispatch_get main_queue(), ^{
    
    
		/*
		 *只在主线程可以执行的处理
		 *例如用户界面更新
		 */
	});
});

上面的就是在后台线程中执行长时间处理,处理结束时,主线程使用该处理结果的源代码。

dispatch_async(queue, ^{
    
    

这仅有一行的代码让处理在后台线程中执行。

dispatch_async(dispatch_get main_queue(), ^{
    
    

这样,仅此一行代码就能够让处理在主线程中执行。另外,大家看到“^”符号就能发现,GCD使用了“Blocks”,进一步简化了应用程序代码。
在导入GCD之前,Cocoa框架提供了NSObject类的performSelectorInBackground:withObject 实例方法和 performSelectorOnMainThread 实例方法等简单的多线程编程技术。
performSelector 系方法确实要比使用NSThread 类进行多线程编程简单,但与之前使用GCD的源代码相比,结果一目了然。相比performSelector系方法,GCD 更为简洁。如果使用GCD,不仅不必使用NSThread类或performSelector系方法这些过时的API,更可以通过GCD提供的系统级线程管理提高执行效率。真是到处都是优点呀。

(二)多线程编程

线程到底是什么呢?我们来温习一下。先看一下下面的Objective-C 源代码。

int main() {
    
    
	id o = [[MyObject alloc] init];
	[o execBlock];
	return 0;
}

虽然调用了几个方法,但代码行基本上是按从上到下的顺序执行的。那么,该源代码实际上在Mac或iPhone上是如何执行的呢?该源代码通过编译器转换为CPU命令列(二进制代码)。

汇集CPU命令列和数据,将其作为一个应用程序安装到 Mac 或 iPhone 上。
Mac、iPhone的操作系统OSXiOS根据用户的指示启动该应用程序后,首先便将包含在应用程序中的CPU命令列配置到内存中。CPU从应用程序指定的地址开始,一个一个地执行CPU命令列。先执行地址 lac 的命令列push,接着向后移动,执行地址 lae 的命令列 movw,再次向后移动,执行地址lb2的命令列,就这样不断循环下去。
在 Objective-C 的 if 语句和 for 语句等控制语句或函数调用的情况下,执行命令列的地址会远离当前的位置(位置迁移)。但是,由于一个CPU一次只能执行一个命令,不能执行某处分开的并列的两个命令,因此通过CPU执行的CPU命令列就好比一条无分叉的大道,其执行不会出现分歧。
在这里插入图片描述
这里所说的“1个CPU执行的CPU命令列为一条无分叉路径”即为“线程”。
现在一个物理的 CPU 芯片实际上有64个(64核)CPU,如果1个CPU核虚拟为两个CPU核工作,那么一台计算机上使用多个CPU核就是理所当然的事了。尽管如此,“1个CPU核执行的CPU 命令列为一条无分叉路径”仍然不变。
这种无分叉路径不只1条,存在有多条时即为“多线程”。在多线程中,1个CPU核执行多条不同路径上的不同命令。
在这里插入图片描述
虽然CPU的相关技术有很多,其进步也令人眼花缭乱,但基本上1个CPU核一次能够执行的CPU命令始终为1。那么怎样才能在多条路径中执行CPU命令列呢?
OS X和iOS 的核心XNU内核在发生操作系统事件时(如每隔一定时间,唤起系统调用等情况)会切换执行路径。执行中路径的状态,例如CPU的寄存器等信息保存到各自路径专用的内存块中,从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的 CPU命令列。这被称为“上下文切换”。
由于使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上去就好像1个CPU核能够并列地执行多个线程一样。而且在具有多个 CPU核的情况下,就不是“看上去像”了,而是真的提供了多个CPU核并行执行多个线程的技术。
这种利用多线程编程的技术就被称为“多线程编程”。
但是,多线程编程实际上是一种易发生各种问题的编程技术。比如多个线程更新相同的资源会导致数据的不一致(数据竞争)、停止等待事件的线程会导致多个线程相互持续等待(死锁)、使用太多线程会消耗大量内存等。
当然,要回避这些问题有许多方法,但程序都偏于复杂。
尽管极易发生各种问题,也应当使用多线程编程。这是为什么呢?因为使用多线程编程可保证应用程序的响应性能。
应用程序在启动时,通过最先执行的线程,即“主线程”来描绘用户界面、处理触摸屏幕的事件等。如果在该主线程中进行长时间的处理,如 AR 用画像的识别或数据库访问,就会妨碍主线程的执行(阻塞)。在OS X和iOS 的应用程序中,会妨碍主线程中被称为RunLoop 的主循环的执行,从而导致不能更新用户界面、应用程序的画面长时间停滞等问题。
这就是长时间的处理不在主线程中执行而在其他线程中执行的原因。
在这里插入图片描述

使用多线程编程,在执行长时间的处理时仍可保证用户界面的响应性能。
GCD大大简化了偏于复杂的多线程编程的源代码。

二、GCD的API

(一)Dispatch Queue

首先回顾一下苹果官方对GCD的说明。
开发者要做的只是定义想执行的任务并追加到适当的 Dispatch Queue 中。
这句话用源代码表示如下:

dispatch_async(queue, ^{
    
    
	/*
	 *想执行的任务
	 */
});

该源代码使用 Block 语法“定义想执行的任务”,通过dispatch_async 函数“追加”赋值在变量 queue 的“Dispatch Queue 中”。仅这样就可使指定的 Block 在另一线程中执行。
“Dispatch Queue”是什么呢?如其名称所示,是执行处理的等待队列。应用程序编程人员通过 dispatch_async 函数等 API,在 Block 语法中记述想执行的处理并将其追加到 Dispatch Queue 中。Dispatch Queue 按照追加的顺序(先进先出 FIFO,First-In-First-Out)执行处理。

在这里插入图片描述
另外在执行处理时存在两种Dispatch Queue,一种是等待现在执行中处理的 Serial Dispatch Queue,另一种是不等待现在执行中处理的 Concurrent Dispatch Queue

Dispatch Queue种类 说明
Serial Dispatch Queue 等待现在执行中处理结束
Concurrent Dispatch Queue 不等待现在执行中处理结束

在这里插入图片描述
比较这两种 Dispatch Queue。准备以下源代码,在dispatch_async 中追加多个处理。

dispatch_async(queue,blk0);
dispatch_async(queue, blk1);
dispatch_async(queue,blk2);
dispatch_async(queue,blk3);
dispatch_async(queue,blk4);
dispatch_async(queue, blk5);
dispatch_async(queue,blk6);
dispatch_async(queue,blk7);

当变量 queueSerial Dispatch Queue 时,因为要等待现在执行中的处理结束,所以首先执行blk0,blk0 执行结束后,接着执行 blk1,blk1 结束后再开始执行blk2,如此重复。同时执行的处理数只能有1个。即执行该源代码后,一定按照以下顺序进行处理。

blk0
blk1
blk2
blk3
blk4
blk5
blk6
blk7

当变量queueConcurrent Dispatch Queue 时,因为不用等待现在执行中的处理结束,所以首先执行bk0,不管bk0 的执行是否结束,都开始执行后面的blk1,不管 blk1 的执行是否结束,都开始执行后面的blk2,如此重复循环。
这样虽然不用等待处理结束,可以并行执行多个处理,但并行执行的处理数量取决于当前系统的状态。即 iOS 和 OS X基于Dispatch Queue 中的处理数、CPU 核数以及 CPU 负荷等当前系统的状态来决定 Concurrent Dispatch Queue 中并行执行的处理数。所谓“并行执行”,就是使用多个线程同时执行多个处理。
在这里插入图片描述
iOS 和 OS X 的核心–XNU 内核决定应当使用的线程数,并只生成所需的线程执行处理。另外,当处理结束,应当执行的处理数减少时,XNU 内核会结束不再需要的线程。XNU内核仅使用Concurrent Dispatch Queue 便可完美地管理并行执行多个处理的线程。
例如,前面的源代码,在多个线程中执行 Block。

线程0 线程1 线程2 线程3
blk0 blk1 blk2 blk3
blk4 blk6 blk5
blk7

假设准备4个Concurrent Dispatch Queue 用线程。首先blk0在线程0中开始执行,接着 blk1在线程1中、blk2在线程2中、blk3在线程3中开始执行。线程0中blk0执行结束后开始执行 blk4,由于线程1中blk1的执行没有结束,因此线程2中blk2执行结束后开始执行 blk5,就这样循环往复。
像这样在Concurrent Dispatch Queue 中执行处理时,执行顺序会根据处理内容和系统状态发生改变。它不同于执行顺序固定的Serial Dispatch Queue。在不能改变执行的处理顺序或不想并行执行多个处理时使用Serial Dispatch Queue
虽然知道了有 Serial Dispatch QueueConcurrent Dispatch Queue 这两种,但如何才能得到这些Dispatch Queue 呢?方法有两种。

(二)dispatch_queue_create

第一种方法是通过GCD的API生成Dispatch Queue
通过 dispatch_queue_create 函数可生成 Dispatch Queue。以下源代码生成了 Serial Dispatch Queue

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);

在说明dispatch_queue_create 函数之前,先讲一下关于 Serial Dispatch Queue 生成个数的注意事项。如前所述,Concurrent Dispatch Queue 并行执行多个追加处理,而Serial Dispatch Queue 同时只能执行1个追加处理。虽然 Serial Dispatch QueueConcurrent Dispatch Queue 受到系统资源的限制,但用dispatch_queue_create 函数可生成任意多个 Dispatch Queue
当生成多个Serial Dispatch Queue 时,各个 Serial Dispatch Queue 将并行执行。虽然在1个Serial Dispatch Queue 中同时只能执行一个追加处理,但如果将处理分别追加到4个Serial Dispatch Queue 中,各个Serial Dispatch Queue 执行1个,即为同时执行4个处理。
在这里插入图片描述
以上是关于 Serial Dispatch Queue 生成个数注意事项的说明。一旦生成 Serial Dispatch Queue并追加处理,系统对于一个 Serial Dispatch Queue 就只生成并使用一个线程。如果生成2000个 Serial Dispatch Queue,那么就生成2000个线程。
像之前列举的多线程编程问题一样,如果过多使用多线程,就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能。
只在为了避免多线程编程问题之一:多个线程更新相同资源导致数据竞争时使用 Serial Dispatch Queue
在这里插入图片描述
但是Serial Dispatch Queue 的生成个数应当仅限所必需的数量。例如更新数据库时1个表生成1个Serial Dispatch Queue,更新文件时1个文件或是可以分割的1个文件块生成1个Serial Dispatch Queue。虽然“Serial Dispatch QueueConcurrent Dispatch Queue 能生成更多的线程”,但绝不能激动之下大量生成Serial Dispatch Queue
当想并行执行不发生数据竞争等问题的处理时,使用Concurrent Dispatch Queue。而且对于 Concurrent Dispatch Queue 来说,不管生成多少,由于XNU内核只使用有效管理的线程,因此不会发生 Serial Dispatch Queue 的那些问题。
下面我们回来继续讲dispatch_queue_create 函数。该函数的第一个参数指定 Serial Dispatch Queue的名称。像此源代码这样,Dispatch Queue的名称推荐使用应用程序ID这种逆序全程域名(FQDN,fully qualified domain name)。该名称在Xcode 和 Instruments的调试器中作为Dispatch Queue 名称表示。另外,该名称也出现在应用程序崩溃时所生成的CrashLog中。我们命名时应遵循这样的原则:对我们编程人员来说简单易懂,对用户来说也要易懂。如果嫌命名麻烦设为 NULL也可以,但你在调试中一定会后悔没有为Dispatch Queue 署名。
生成 Serial Dispatch Queue 时,像该源代码这样,将第二个参数指定为NULL。生成 Concurrent Dispatch Queue 时,像下面源代码一样,指定为DISPATCH_QUEUE_CONCURRENT

dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.example.gcd.MyConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_queue_create 函数的返回值为表示 Dispatch Queue 的“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 Dispatch Queue 中执行指定的Block。
另外,遗憾的是尽管有 ARC 这一通过编译器自动管理内存的优秀技术,但生成的Dispatch Queue 必须由程序员负责释放。这是因为Dispatch Queue 并没有像Block那样具有作为 Objective-C 对象来处理的技术。
通过dispatch_queue_create 函数生成的 Dispatch Queue 在使用结束后通过 dispatch_release 函数释放。
注意:在iOS6.0后 ,GCD对象使用ARC 的管理,ARC程序中不再需要调用dispatch_release来释放GCD对象

dispatch_release(mySerialDispatchQueue);

该名称中含有release,由此可以推测出相应地也存在 dispatch_retain 函数。

dispatch_retain(myConcurrentDispatchQueue);

Dispatch Queue 也像Objective-C的引用计数式内存管理一样,需要通过dispatch_retain 函数和dispatch_release函数的引用计数来管理内存。前面的源代码中,需要释放通过 dispatch_queue_create函数生成并赋值给变量myConcurrentDispatchQueue 中的 Concurrent Dispatch Queue

dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create(com.example.gcd.MyConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(MyConcurrentDispatchQueue, ^{
    
    
	NSLog(@"block on myConcurrent DispatchQueue");
});

dispatch_release(myConcurrentDispatchQueue);

虽然Concurrent Dispatch Queue是使用多线程执行追加的处理,但像该例这样,在dispatch_sync函数中追加Block到Concurrent Dispatch Queue,并立即通过dispatch_release 函数进行释放是否可以呢?
源代码完全没有问题。在dispatch_async函数中追加Block到Dispatch Queue,换言之,该 Block 通过 dispatch_retain 函数持有 Dispatch Queue。无论 Dispatch QueueSerial Dispatch Queue还是Concurrent Dispatch Queue都一样。一旦Block 执行结束,就通过 dispatch_release 函数释放该 Block 持有的 Dispatch Queue
也就是说,在dispatch_async 函数中追加 Block 到 Dispatch Queue 后,即使立即释放 Dispatch Queue,该 Dispatch Queue 由于被 Block 所持有也不会被废弃,因而Block 能够执行。Block 执行结束后会释放Dispatch Queue,这时谁都不持有Dispatch Queue,因此它会被废弃。
另外,能够使用dispatch_retain 函数和 dispatch_release 函数的地方不仅是在 Dispatch Queue中。在之后介绍的几个GCD的API中,名称中含有“create”的 API 在不需要其生成的对象时,有必要通过dispatch_release 函数进行释放。在通过函数或方法获取 Dispatch Queue 以及其他名称中含有create 的 API 生成的对象时,有必要通过dispatch_retain 函数持有,并在不需要时通过 dispatch_release 函数释放。

(三)Main Dispatch Queue / Global Dispatch Queue

第二种方法是获取系统标准提供的 Dispatch Queue
实际上不用特意生成Dispatch Queue 系统也会给我们提供几个。那就是Main Dispatch QueueGlobal Dispatch Queue
Main Dispatch Queue 正如其名称中含有的“Main”一样,是在主线程中执行的Dispatch Queue因为主线程只有1个,所以Main Dispatch Queue 自然就是Serial Dispatch Queue
追加到Main Dispatch Queue 的处理在主线程的RunLoop 中执行。由于在主线程中执行,因此要将用户界面的界面更新等一些必须在主线程中执行的处理追加到 Main Dispatch Queue 使用。这正好与NSObject类的performSelectorOnMainThread 实例方法这一执行方法相同。
在这里插入图片描述

另一个Global Dispatch Qucue 是所有应用程序都能够使用的Concurrent Dispatch Queue没有必要通过dispatch_queue_create 函数逐个生成Concurrent Dispatch Queue。只要获取 Global Dispatch Queue 使用即可。
另外,Global Dispatch Queue 有4个执行优先级,分别是高优先级(High Priority)、默认优先级(Default Priority)、低优先级(Low Priority)和后台优先级(Background Priority)。通过 XNU内核管理的用于Global Dispatch Queue 的线程,将各自使用的Global Dispatch Queue 的执行优先级作为线程的执行优先级使用。在向Global Dispatch Queue 追加处理时,应选择与处理内容对应的执行优先级的 Global Dispatch Queue
但是通过XNU内核用于GlobalDispatch Queue的线程并不能保证实时性,因此执行优先级只是大致的判断。例如在处理内容的执行可有可无时,使用后台优先级的Global Dispatch Queue等,只能进行这种程度的区分。
系统提供的 Dispatch Queue

名称 Dispatch Queue的种类 说明
Main Dispatch Queue Serial Dispatch Queue 主线程执行
Global Dispatch Queue(High Priority) Concurrent Dispatch queue 执行优先级:高(最高优先)
Global Dispatch Queue(Default Priority) Concurrent Dispatch queue 执行优先级:默认
Global Dispatch Queue(Low Priority) Concurrent Dispatch queue 执行优先级:低
Global Dispatch Queue(Background Priority ) Concurrent Dispatch queue 执行优先级:后台

各种 Dispatch Queue的获取方法如下

/*
 * Main Dispatch Queue的获取方法
 */
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
/*
 *Global Dispatch Queue(高优先级)的获取方法
 */
dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, O);
/*
 * Global Dispatch Queue(默认优先级)的获取方法
 */
dispatch_queue_t globalDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, O);
/*
 *Global Dispatch Queue(低优先级)的获取方法
 */
dispatch_queue_t globalDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, O);
/*
 * Global Dispatch Queue(后台优先级)的获取方法
 */
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, O);

另外,对于Main Dispatch QueueGlobal Dispatch Queue 执行 dispatch_retain 函数和 dispatch_release 函数不会引起任何变化,也不会有任何问题。这也是获取并使用Global Dispatch Queue 比生成、使用、释放 Concurrent Dispatch Queue 更轻松的原因。
当然,源代码上在进行类似通过dispatch_queue_create 函数生成Dispatch Queue 的处理要更轻松时,可参照引用计数式内存管理的思考方式,直接在Main Dispatch QueueGlobal Dispatch Queue 中执行 dispatch_retain 函数和 dispatch_release 函数。
以下列举使用了Main Dispatch QueueGlobal Dispatch Queue 的源代码:

/*
 *在默认优先级的Global Dispatch Queue中执行Block
 */
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, O), ^{
    
    
	/*
	 * 可并行执行的处理
	 */
	/*
	 * 在Main Dispatch Queue中执行Block
	 */
	dispatch_async(dispatch_get_main_queue(), ^{
    
    
		/*
	 	 * 只能在主线程中执行的处理
	 	 */
	});
}) ;

(四)dispatch_set_target_queue

dispatch_queue_create 函数生成的 Dispatch Queue 不管是Serial Dispatch Queue 还是 Concurrent Dispatch Queue,都使用与默认优先级Global Dispatch Queue 相同执行优先级的线程。而变更生成的Dispatch Queue 的执行优先级要使用dispatch_set_target_queue 函数。在后台执行动作处理的 Serial Dispatch 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, O);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);

指定要变更执行优先级的Dispatch Queuedispatch_set_target_queue 函数的第一个参数,指定与要使用的执行优先级相同优先级的Global Dispatch Queue为第二个参数(目标)。第一个参数如果指定系统提供的Main Dispatch QueueGlobal Dispatch Queue 则不知道会出现什么状况,因此这些均不可指定。
Dispatch Queue 指定为dispatch_set_target_queue 函数的参数,不仅可以变更 Dispatch Queue的执行优先级,还可以作成Dispatch Queue的执行阶层。如果在多个Serial Dispatch Queue 中用 dispatch_set_target_queue 函数指定目标为某一个Serial Dispatch Queue,那么原先本应并行执行的多个Serial Dispatch Queue,在目标Serial Dispatch Queue 上只能同时执行一个处理。
在这里插入图片描述
在必须将不可并行执行的处理追加到多个 Serial Dispatch Queue 中时,如果使用dispatch_set_target_queue 函数将目标指定为某一个 Serial Dispatch Queue,即可防止处理并行执行。

(五)dispatch_after

经常会有这样的情况:想在3秒后执行处理。可能不仅限于3秒,总之,这种想在指定时间后执行处理的情况,可使用dispatch_after 函数来实现。
在3秒后将指定的Block追加到Main Dispatch Queue 中的源代码如下:

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3u11 * NSEC_PER_SEC);

dispatch_after(time, dispatch_get_main_queue(), ^{
    
    
	NSLog(@"waited at least three seconds.");
});

需要注意的是,dispatch_after函数并不是在指定时间后执行处理,而只是在指定时间追加处理到Dispatch Queue。此源代码与在3秒后用dispatch_async 函数追加Block到Main Dispatct Queue 的相同。
因为Main Dispatch Queue 在主线程的RunLoop中执行,所以在比如每隔1/60秒执行的 RunLoop中,Block最快在3秒后执行,最慢在3秒+1/60秒后执行,并且在Main Dispatch Queue有大量处理追加或主线程的处理本身有延迟时,这个时间会更长。
虽然在有严格时间的要求下使用时会出现问题,但在想大致延迟执行处理时,该函数是非常有效的。
另外,第二个参数指定要追加处理的Dispatch Queue,第三个参数指定记述要执行处理的 Block。
第一个参数是指定时间用的dispatch_time_t类型的值。该值使用dispatch_time 函数或 dispatch_walltime 函数作成。
dispatch_time函数能够获取从第一个参数dispatch_time_t类型值中指定的时间开始,到第二个参数指定的毫微秒单位时间后的时间。第一个参数经常使用的值是之前源代码中出现的 DISPATCH_TIME_NOW。这表示现在的时间。即以下源代码可得到表示从现在开始1秒后的 dispatch_time_t类型的值。

dispatch_time_t time = dispatch_time(DISPATCH TIME_NOW, 1ull* NSEC_PER_SEC)

数值和NSEC_PER_SEC的乘积得到单位为毫微秒的数值。“ull”是C语言的数值字面量,是显式表明类型时使用的字符串(表示“unsigned long long”)。如果使用NSEC_PER_MSEC 可以以毫秒为单位计算。以下源代码获取表示从现在开始150毫秒后时间的值。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 150U11 * NSEC_PER_MSEC);

dispatch_walltime 函数由POSIX中使用的struct timcspec类型的时间得到dispatch_time_t类型的值。dispatch_time 函数通常用于计算相对时间,而dispatch_walltime 函数用于计算绝对时间。例如在dispatch_after函数中想指定 2111年11月 11日 11 时 11 分 11 秒这一绝对时间的情况,这可作为粗略的闹钟功能使用。
struct timespec 类型的时间可以很轻松地通过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;
}

上面源代码可由NSDate类对象获取能传递给dispatch_after函数的dispatch_time_t类型的值。

(六)Dispatch Group

在追加到 Dispatch Queue 中的多个处理全部结束后想执行结束处理,这种情况会经常出现。使用一个Serial Dispatch Queue 时,只要将想执行的处理全部追加到该 Serial Dispatch Queue中并在最后追加结束处理,即可实现。但是在使用Concurrent Dispatch Queue 时或同时使用多个 Dispatch Qucue 时,源代码就会变得颇为复杂。
在此种情况下使用Dispatch Group。例如下面源代码为:追加3个Block到 Global Dispatch Queue,这些Block如果全部执行完毕,就会执行Main Dispatch Queue 中结束处理用的 Block。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, O);
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");
});
dispatch_release(group);

该源代码的执行结果如下:

blk1
blk2
blk0
done

因为向Global Dispatch QueueConcurrent Dispatch Queue 追加处理,多个线程并行执,所以追加处理的执行顺序不定。执行时会发生变化,但是此执行结果的done 一定是最后输出的。
无论向什么样的Dispatch Queue 中追加处理,使用Dispatch Group都可监视这些处理执行的结束。一旦检测到所有处理执行结束,就可将结束的处理追加到Dispatch Queue 中。这就是使用 Dispatch Group 的原因。
dispatch_group_create 函数生成dispatch_group_t类型的Dispatch Group。如 dispatch_group_create 函数名中所含的create 所示,该 Dispatch GroupDispatch Queue 相同,在使用结束后需要通过dispatch_release 函数释放。
dispatch_group_async 函数 与 dispatch_async 函数相同,都追加 Block 到指定的 Dispatch Queue 中。与 dispatch_async 函数不同的是指定生成的 Dispatch Group 为第一个参数。指定的 Block 属于指定的 Dispatch Group
另外,与追加Block 到 Dispatch Queue 时同样,Block 通过 dispatch_retain 函数持有Dispatch Group,从而使得该Block属于Dispatch Group。这样如果Block执行结束,该Block就通过 dispatch_release 函数释放持有的 Dispatch Group。一旦 Dispatch Group 使用结束,不用考虑属于该 Dispatch Group 的 Block,立即通过 dispatch_release 函数释放即可。
在追加到 Dispatch Group 中的处理全部执行结束时,该源代码中使用的 dispatch_group_notify函数会将执行的 Block 追加到 Dispatch Queue,将第一个参数指定为要监视的 Dispatch Group。在追加到该 Dispatch Group 的全部处理执行结束时,将第三个参数的 Block 追加到第二个参数 Dispatch Queue 中。在dispatch_group_notify 函数中不管指定什么样的Dispatch Queue,属于 Dispatch Group 的全部处理在追加指定的Block 时都已执行结束。
另外,在Dispatch Group 中也可以使用dispatch_group_wait 函数仅等待全部处理执行结束。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, O);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    
    
	NSLog(@"blk0");
});
dispatch_group_async(group, queue, ^{
    
    
	NSLog(@"blkl");
});
dispatch_group_async(group, queue, ^{
    
    
	NSLog(@"blk2");
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER); dispatch_release(group);

dispatch_group_wait 函数的第二个参数指定为等待的时间(超时)。它属于 dispatch_time_t类型的值。该源代码使用DISPATCH_TIME_FOREVER,意味着永久等待。只要属于Dispatch Group的处理尚未执行结束,就会一直等待,中途不能取消。
如同dispatch_after 函数说明中出现的那样,指定等待间隔为1秒时应做如下处理。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1U11 * NSEC_PER_SEC);
long result * dispatch_group_wait(group, time);
if (result == 0) {
    
    
	/*属于Dispatch Group的全部处理执行结束*/
} else {
    
    
	/*
	 *属于Dispatch Group的某一个处理还在执行中
	 */
}

如果dispatch_group_wait 函数的返回值不为0,就意味着虽然经过了指定的时间,但属于 Dispatch Group的某一个处理还在执行中。如果返回值为0,那么全部处理执行结束。当等待时间为DISPATCH TIME FOREVER、由dispatch_group_wait函数返回时,由于属于Dispatch Group的处理必定全部执行结束,因此返回值恒为0。
这里的“等待”是什么意思呢?这意味着一旦调用dispatch_group_wait 函数,该函数就处于调用的状态而不返回。即执行dispatch_group_wait函数的现在的线程(当前线程)停止。在经过 dispatch_group_wait 函数中指定的时间或属于指定Dispatch Group的处理全部执行结束之前,执行该函数的线程停止。
指定DISPATCH_TIME_NOW,则不用任何等待即可判定属于Dispatch Group的处理是否执行结束。

long result = dispatch_group_wait(group, DISPATCH_TIME_NOW);

在主线程的RunLoop的每次循环中,可检查执行是否结束,从而不耗费多余的等待时间,虽然这样也可以,但一般在这种情形下,还是推荐用dispatch_group_notify函数追加结束处理到 Main Dispatch Queue 中。这是因为dispatch_group_notify 函数可以简化源代码。

(七)dispatch_barrier_async

在访问数据库或文件时,如前所述,使用 Serial Dispatch Queue 可避免数据竞争的问题。
写入处理确实不可与其他的写入处理以及包含读取处理的其他某些处理并行执行。但是如果读取处理只是与读取处理并行执行,那么多个并行执行就不会发生问题。
也就是说,为了高效率地进行访问,读取处理追加到Concurrent Dispatch Queue中,写入处理在任一个读取处理没有执行的状态下,追加到Serial Dispatch Queue中即可(在写入处理结束之前,读取处理不可执行)。
虽然利用Dispatch Groupdispatch_set_target_queue 函数也可实现,但是源代码会很复杂 GCD为我们提供了更为聪明的解决方法-dispatch_barrier_async 函数。该函数同 dispatch_queue_create 函数生成的Concurrent_Dispatch_Queue 一起使用。
首先dispatch_queue_create 函数生成Concurrent_Dispatch_Queue,在dispatch_async 中追加读取处理。

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.ForBarrier", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blkl_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading)	
dispatch_async(queue, blk6_for_reading);
dispatch_async(queue, blk7_for_reading);

dispatch_release(queue);

blk3_for_reading 处理和blk4_for_reading 处理之间执行写入处理,并将写入的内容读取 blk4_for_reading 处理以及之后的处理中。

dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
/*
 *写入处理
 *将写入的内容读取之后的处理中
 */
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading):
dispatch_async(queue, blk7_for_reading);

如果像下面这样简单地在dispatch_async 函数中加入写入处理,那么根据Concurrent Dispatch Queue的性质,就有可能在追加到写入处理前面的处理中读取到与期待不符的数据,还可能因非法访问导致应用程序异常结束。如果追加多个写入处理,则可能发生更多问题,比如数据竞争等。

dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);

因此我们要使用 dispatch_barrier_async 函 数。dispatch_barrier_async 函数会等待 追 加 到 Concurrent Dispatch Queue上的并行执行的处理全部结束之后,再将指定的处理追加到该 Concurrent Dispatch Queue 中。然后在由dispatch_barrier_async 函数追加的处理执行完毕后,Concurrent Dispatch Queue 才恢复为一般的动作,追加到该 Concurrent Dispatch Queue 的处理又开始并行执行。

dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_barrier_async(queue,blk_for_writing); dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);
dispatch_async(queue, blk7_for_reading);

如上所示,使用方法非常简单。仅使用dispatch_barrier_async 函数代替 dispatch_async 函数即可。
在这里插入图片描述
使用 Concurrent Dispatch Queuedispatch_barrier_async 函数可实现高效率的数据库访问和文件访问。

(八)dispatch_sync

dispatch_async 函数的“async”意味着“非同步”(asynchronous),就是将指定的Block“同步”地追加到指定的Dispatch Queue中。dispatch_async函数不做任何等待。
在这里插入图片描述

既然有“async”,当然也就有“sync”,即dispatch_sync函数。它意味着“同步”(synchronous),也就是将指定的Block“同步”追加到指定的Dispatch Queue中。在追加Block结束之前,dispatch_sync 函数会一直等待
在这里插入图片描述

dispatch_group_wait 函数说明所示(参考3.2.6 节),“等待”意味着当前线程停止。
我们先假设这样一种情况:执行Main Dispatch Queue 时,使用另外的线程Global Dispatch Queue 进行处理,处理结束后立即使用所得到的结果。在这种情况下就要使用 dispatch_sync 函数。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, O);
dispatch_sync(queue, ^{
    
    
	/* 处理 */
});

一旦调用dispatch_sync 函数,那么在指定的处理执行结束之前,该函数不会返回。dispatch_sync 函数可简化源代码,也可说是简易版的dispatch_group_wait 函数。
正因为dispatch_sync 函数使用简单,所以也容易引起问题,即死锁。例如如果在主线程中执行以下源代码就会死锁。

dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_sync(queue, ^{
    
    
	NSLog(@"Hello?");
});

该源代码在 Main Dispatch Queue 即主线程中执行指定的 Block,并等待其执行结束。而其实在主线程中正在执行这些源代码,所以无法执行追加到 Main Dispatch Queue 的 Block。下面例子也一样。

dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_async(queue, ^{
    
    
	dispatch_sync(queue, ^{
    
    
		NSLog(@"Hello?");
	});
});

Main Dispatch Queue 中执行的Block等待Main Dispatch Queue 中要执行的Block 执行结束。这样的死锁就像在画像上画画一样。
当然 Serial Dispatch Queue 也会引起相同的问题。

dispatch_queue_t_queue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
dispatch_async(queue, ^{
    
    
	dispatch_sync(queue, ^{
    
    
		NSLog(@"Hello?");
	});
});

另外,由dispatch_barrier_async 函数中含有 async 可推测出,相应的也有dispatch_barrier_sync函数。dispatch_barrier_async 函数的作用是在等待追加的处理全部执行结束后,再追加处理到Dispatch Queue 中,此外,它还与dispatch_sync 函数相同,会等待追加处理的执行结束。
在今后的编程中,大家最好在深思熟虑、想好希望达到的目的之后再使用dispatch_sync 函数等同步等待处理执行的API。因为使用这种API时,稍有不慎就会导致程序死锁,我想大家都不希望发生这种情况吧。

(九)dispatch_apply

dispatch_apply 函数是 dispatch_sync 函数和 Dispatch Group 的关联 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");

执行结果:

6
0
4
1
3
5
2
7
8
9
done

因为在Global Dispatch Queue 中执行处理,所以各个处理的执行时间不定。但是输出结果中最后的done 必定在最后的位置上。这是因为dispatch_apply函数会等待全部处理执行结束。
第一个参数为重复次数,第二个参数为追加对象的Dispatch Queue,第三个参数为追加的处。与到目前为止所出现的例子不同,第三个参数的Block为带有参数的 Block。这是为了按第一个参数重复追加Block并区分各个Block而使用。例如要对NSArray类对象的所有元素执行处理时,不必一个一个编写for 循环部分。
我们来看一下下面的源代码。变量array为NSArray类对象。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_apply([array count], queue, ^(size_t index) {
    
    
	NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
});

这样可简单地在Global Dispatch Queue 中对所有元素执行Block。
另外,由于dispatch_apply 函数也与 dispatch_sync 函数相同,会等待处理执行结束,因此推荐在 dispatch_async 函数中非同步地执行 dispatch_apply 函数。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
 * 在Global Dispatch Queue中非同步执行
 */
dispatch_async(queue, ^{
    
    
	/*
	 * Global Dispatch Queue
	 *等待dispatch apply函数中全部处理执行结束
	 */
	dispatch_apply([array count], queue, ^(size_t index){
    
    
		/*
	 	 *并列处理包含在NSArray对象的全部对象
	 	 */
		NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
	});
	/*
	 *dispatch_apply函数中的处理全部执行结束
	 */
	/*
	 *在Main Dispatch Queue 中非同步执行
	 */
	dispatch_async(dispatch_get_main_queue(), ^{
    
    
		/*
		 *在Main Dispatch Queue中执行处理*用户界面更新等
		 */
		NSLog(@"done");
	});
});

(十)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 Queuedispatch_barrier_async 函数可避免这类问题,但有必要进行更细粒度的排他控制。
我们来思考一下这种情况:不考虑顺序,将所有数据追加到 NSMutableArray 中。

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进行说明。
Dispatch Semaphore持有计数的信号,该计数是多线程编程中的计数类型信号。所谓信号,类似于过马路时常用的手旗。可以通过时举起手旗,不可通过时放下手旗。而在Dispatch Semaphore中,使用计数来实现该功能。计数为0时等待,计数为1或大于1时,减去1而不等待。
下面介绍一下使用方法。通过dispatch_semaphore_create 函数生成Dispatch Semaphore

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

参数表示计数的初始值。本例将计数值初始化为“1”。从函数名称中含有的create 可以看出,该函数与Dispatch QueueDispatch Group 一样,必须通过dispatch_release 函数释放。当然也可通过dispatch_retain 函数持有。

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

dispatch_semaphore_wait 函数等待 Dispatch Semaphore 的计数值达到大于或等于1。当计数值大于等于 1,或者在待机中计数值大于等于1时,对该计数进行减法并从 dispatch_semaphore_wait 函数返回。第二个参数与dispatch_group_wait 函数等相同,由 dispatch_time_t类型值指定等待时间。该例的参数意味着永久等待。另外,dispatch_semaphore_wait 函数的返回值也与 dispatch_group_wait 函数相同。可像以下源代码这样,通过返回值进行分支处理。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_semaphore_wait(semaphore, time);

if (result == 0) {
    
    
	/*
	 * 由于Dispatch Semaphore的计数值达到大于等于1
	 * 或者在待机中的指定时间内
	 * Dispatch Semaphore的计数值达到大于等于1
	 * 所以Dispatch Semaphore的计数值减去1。
	 * 可执行需要进行排他控制的处理
	 */

} else {
    
    
	/*
	 *由于Dispatch Semaphore的计数值为0* 因此在达到指定时间为止待机
	 */
}

dispatch_semaphore_wait 函数返回0时,可安全地执行需要进行排他控制的处理。该处理结束时通过dispatch_semaphore_signal 函数将 Dispatch Semaphore 的计数值加1。
我们在前面的源代码中实际使用 Dispatch Semaphore 看看。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
 * 生成Dispatch Semaphoreo
 * Dispatch Semaphore的计数初始值设定为“1”。
 * 保证可访问NSMutableArray类对象的线程*同时只能有1个。
 */
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array =[[NSMutableArray alloc] init];
for(int i=0; i <100000; ++i){
    
    
	dispatch_async(queue, ^{
    
    
		/*
		 *等待Dispatch Semaphore.
		 *一直等待,直到Dispatch Semaphore的计数值达到大于等于1。
		 */
	dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
	/*
	 * 由于Dispatch Semaphore的计数值达到大于等于1
	 * 所以将Dispatch Semaphore的计数值减去1, 
	 * dispatch_semaphore_wait 函数执行返回。
	 * 即执行到此时的
	 * Dispatch Semaphore的计数值恒为“0”。
	 * 由于可访问NSMutableArray类对象的线程只有1个
	 * 因此可安全地进行更新。
	 */
	[array addobject:[NSNumber numberWithInt:i]];
	/*
	 * 排他控制处理结束,
	 * 所以通过dispatch semaphore signal 函数
	 * 将 Dispatch Semaphore的计数值加1
	 * 
	 * 如果有通过dispatch semaphore wait 函数
	 * 等待Dispatch Semaphore的计数值增加的线程,
	 * 就由最先等待的线程执行。
	 */
	dispatch_semaphore_signal(semaphore);
));
/*
 *如果使用结束,需要如以下这样
 *释放 Dispatch Semaphoxe
 *dispatch_release(bemaphore);
 */

在没有 Serial Dispatch Queuedispatch_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函数,该源代码即使在多线程环境下执行,也可保证百分之百安全。
之前的源代码在大多数情况下也是安全的。但是在多核CPU中,在正在更新表示是否初始化的标志变量时读取,就有可能多次执行初始化处理。而用dispatch_once 函数初始化就不必担心这样的问题。这就是所说的单例模式,在生成单例对象时使用。

(十三)Dispatch I/O

大家可能想过,在读取较大文件时,如果将文件分成合适的大小并使用Global Dispatch Queue 并列读取的话,应该会比一般的读取速度快不少。现今的输入/输出硬件已经可以做到一次使用多个线程更快地并列读取了。能实现这一功能的就是Dispatch I/O和 Dispatch Data。
通过Dispatch I/O 读写文件时,使用Global Dispatch Queue 将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_async(queue, ^{
    
    /*读取40960 ~ 49151字节*/});
dispatch_async(queue, ^{
    
    /*读取49152 ~ 57343字节*/});
dispatch_async(queue, ^{
    
    /*读取57344 ~ 65535字节*/});

可像上面这样,将文件分割为一块一块地进行读取处理。分割读取的数据通过使用Dispatch Data 可更为简单地进行结合和分割。
以下为苹果中使用Dispatch I/O和 Dispatch Data 的例子。

pipe_q = dispatch_queue_create("PipeQ", NULL);
pipe_channe1 = dispatch_io_create(DISPATCH_IO_STREAM, fd, pipe_q, ^(int err){
    
    	
	close(fd);
});

*out_fd = fdpair[1];

dispatch_io_set_low_water(pipe_channel, SIZE_MAX);

dispatch_io_read(pipe_channel, 0, SIZE_MAX, pipe_q, ^(bool done, dispatch_data_t pipedata, int err) {
    
     
	if (err == 0) {
    
    
		size_t len = dispatch_data_get_size(pipedata);
		if (len > 0) {
    
    
			const char *bytes = NULL;
			char *encoded;
			dispatch_data_t md = dispatch_data_create_map(pipedata, (const void **)&bytes, &len);
			encoded = asl_core_encode_buffer(bytes, len);
			as1_set((aslmsg)merged_msg, ASL_KEY_AUX_DATA, encoded);
			free(encoded);
			asl_send_message(NULL, merged_msg, -1NULL);
			asl_msg_release(merged_msg);
			dispatch release(md);
		}
	}
	if (done) {
    
    
		dispatch_semaphore_signal(sem);
		dispatch_release(pipe_channel);
		dispatch_release(pipe_q);
	}
});

以上摘自Apple System Log API用的源代码(Libc-763.11 gen/aslc)。dispatch_io_create 函数生成Dispatch I/O,并指定发生错误时用来执行处理的Block,以及执行该Block的 Dispatch Queuedispatch_io_set_low_water 函数设定一次读取的大小(分割大小),dispatch_io_read 函数使用Global Dispatch Queue开始并列读取。每当各个分割的文件块读取结束时,将含有文件块数据的 Dispatch Data 传递给dispatch_io_read函数指定的读取结束时回调用的Block。回调用的 Block 分析传递过来的Dispatch Data并进行结合处理。
如果想提高文件读取速度,可以尝试使用Dispatch I/O。

三、GCD实现

(一)Dispatch Queue

GCD的 Dispatch Queue 非常方便,那么它究竟是如何实现的呢?

  • 用于管理追加的Block的C语言层实现的FIFO队列
  • Atomic函数中实现的用于排他控制的轻量级信号
  • 用于管理线程的C语言层实现的一些容器

不难想象,GCD的实现需要使用以上这些工具。但是,如果仅用这些内容便可实现,那么就不需要内核级的实现了
甚至有人会想,只要努力编写线程管理的代码,就根本用不到GCD。真的是这样吗?我们先来回顾一下苹果的官方说明。
通常,应用程序中编写的线程管理用的代码要在系统级实现。
实际上正如这句话所说,在系统级即iOS和OSX的核心XNU内核级上实现。
因此,无论编程人员如何努力编写管理线程的代码,在性能方面也不可能胜过XNU内核级所实现的GCD。
==使用GCD 要比使用pthreads 和 NSThread 这些一般的多线程编程API 更好。==并且,如果使用 GCD 就不必编写为操作线程反复出现的类似的源代码(这被称为固定源代码片断),而可以在线程中集中实现处理内容,真的是好处多多。我们尽量多使用GCD 或者使用了Cocoa 框架 GCD的NSOperationQueue类等API。
那么首先确认一下用于实现Dispatch Queue而使用的软件组件。

组件名称 提供技术
libdispatch Dispatch Queue
Libc(pthreads) pthread_workqueue
XNU 内核 workqueue

编程人员所使用GCD的API全部为包含在libdispatch库中的C语言函数。Dispatch Queue 通过结构体和链表,被实现为FIFO队列。FIFO队列管理是通过dispatch_async 等函数所追加的Block。

Block 并不是直接加入 FIFO 队列,而是先加入Dispatch Continuation 这一dispatch_continuation_t类型结构体中,然后再加入FIFO队列。该 Dispatch Continuation 用于记忆 Block 所属的 Dispatch Group和其他一些信息,相当于一般常说的执行上下文。
Dispatch Queue 可通过dispatch_set_target_queue 函数设定,可以设定执行该 Dispatch Queue处理的 Dispatch Queue为目标。该目标可像串珠子一样,设定多个连接在一起的Dispatch Queue。但是在连接串的最后必须设定为Main Dispatch Queue,或各种优先级的Global Dispatch Queue,或是准备用于Serial Dispatch Qucue 的各种优先级的Global Dispatch Queue
Main Dispatch Queue 在 RunLoop 中执行Block。这并不是令人耳目一新的技术。 Global 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 这个名称所示,不管系统状态如何,都会强制生成线程的Dispatch Queue
这8种Global Dispatch Queue 各使用1个pthread_workqueue。GCD初始化时,使用pthread_workqueue_create_np 函数生成pthread_workqueue
pthread_workqueue 包含在 Libc 提供的pthreads API 中。其使用bsdthread_registerworkq_open系统调用,在初始化XNU内核的workqueue之后获取 workqueue信息。
XNU内核持有4种 workqueue

  • WORKQUEUE_HIGH_PRIOQUEUE
  • WORKOUEUE_DEFAULT_PRIOQUEUE
  • WORKQUEUE_LOW_PRIOQUEUE
  • WORKQUEUE_BG_PRIOQUEUE

以上为4种执行优先级的workqueue。该执行优先级与Global Dispatch Queue的4种执行优先级相同。
下面看一下DispatchQueue中执行Block的过程。当在Global Dispatch Queue中执行Block时, libdispatch 从Global Dispatch Queue 自身的 FIFO 队列中取出 Dispatch Continuation,调用pthread_workqueue_additem_np函数。将该Global Dispatch Queue 自身、符合其优先级的workqueue信息以及为执行Dispatch Continuation的回调函数等传递给参数。

在这里插入图片描述

pthread_workqueue_additem_np函数使用workq_kernreturn 系统调用,通知workqueue 增加应执行的项目。根据该通知,XNU内核基于系统状态判断是否要生成线程。如果是Overcommit优先级的Global Dispatch Queue,workqueue 则始终生成线程。
该线程虽然与iOS和OSX中通常使用的线程大致相同,但是有一部分pthread API不能使用。详细信息可参考苹果的官方文档《并列编程指南》的“与POSIX线程的互换性”一节。
另外,因为workqueue 生成的线程在实现用于workqueue的线程计划表中运行,所以与一般线程的上下文切换不同。这里也隐藏着使用GCD的原因。
workqueue 的线程执行pthread_workqueue 函数,该函数调用libdispatch 的回调函数。在该回调函数中执行加入到 Dispatch Continuation 的 Block。
lock 执行结束后,进行通知 Dispatch Group 结束、释放 Dispatch Continuation 等处理,开始准备执行加入到Global Dispatch Queue 中的下一个 Block。
以上就是Dispatch Queue 执行的大概过程。
由此可知,在编程人员管理的线程中,想发挥出匹敌GCD的性能是不可能的。

(二)Dispatch Source

GCD中除了主要的Dispatch Queue 外,还有不太引人注目的 Dispatch Source。它是 BSD 系内核惯有功能kqueue的包装。
kqueue 是在XNU内核中发生各种事件时,在应用程序编程方执行处理的技术。其CPU负荷非常小,尽量不占用资源。kqueue 可以说是应用程序处理XNU内核中发生的各种事件的方法中最优秀的一种。
Dispatch Source 可处理以下事件。

名称 内容
DISPATCH_SOURCE_TYPE_DATA_ADD 变量增加
DISPATCH_SOURCE_TYPE_DATA_OR 变量 OR
DISPATCH_SOURCE_TYPE_MACH_SEND MACH 端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH 端口接收
DISPATCH_SOURCE_TYPE_PROC 检测到与进程相关的事件
DISPATCH_SOURCE_TYPE_READ 可读取文件映像
DISPATCH_SOURCE_TYPE_SIGNAL 接收信号
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件系统有变更
DISPATCH_SOURCE_TYPE_WRITE 可写入文件映像

事件发生时,在指定的 Dispatch Queue 中可执行事件的处理。
下面我们使用 DISPATCH_SOURCE_TYPE_READ,异步读取文件映像。

__block size_t total = o;
size_t size = 要读取的字节数;
char *buff =(char *)malloc(size);

/*
 *设定为异步映像
 */
fcntl(sockfd, F_SETFL, O_NONBLOCK);
/*
 *获取用于追加事件处理的Global Dispatch Queue
 */
dispatch_queue_t queue = dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
 *基于READ事件作成Dispatch Source
 */
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, sockfd, 0, queue);
/*
 *指定发生 READ 事件时执行的处理
 */
dispatch_source_set_event_handler(source, ^{
    
    
	/*
	 *获取可读取的字节数
	 */
	size_t available = dispatch_source_get_data(source):
	/*
	 *从映像中读取
	 */
	int length = read (sockfd, buff, available);
	/*
	 *发生错误时取消Dispatch Source
	 */
	if (length < 0) {
    
    
	/*
	 *错误处理
	 */
	dispatch_source_cancel(source);
	total += length;
	if (total == size) {
    
    
		/*
		 *buff的处理
		 */
		/*
		 *处理结束,取消Dispatch Source
		 */
		dispatch_source_cancel(source);
	}
});
/*
 *指定取消 Dispatch Source 时的处理
 */
dispatch_source_set_cancel_handler(source, ^{
    
    
	free(buff);
	close(sockfd);
	/*
	 *释放Dispatch Source(自身)
	 */
	dispatch_release(source);
});
/*
*启动Dispatch Source
*/
dispatch_resume(source);

与上面源代码非常相似的代码,使用在了Core Foundation框架的用于异步网络的API CFSocket中。因为Foundation 框架的异步网络 API 是通过CFSocket 实现的,所以可享受到仅使用 Foundation 框架的 Dispatch Source(即GCD)带来的好处。
最后给大家展示一个使用了 DISPATCH_SOURCE_TYPE_TIMER 的定时器的例子。在网络编程的通信超时等情况下可使用该例。

/*
 *指定 DISPATCH_SOURCE_TYPE_TIMER,作成 Dispatch Source.
 *在定时器经过指定时间时设定Main Dispatch Queue为追加处理的Dispatch Queue
 */
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue ());
/*
 *将定时器设定为15秒后。不指定为重复。
 *允许迟廷 1秒。
 */
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 15ul1 *NSEC PER SEC), DISPATCH_TIME_FOREVER, 1UlL * NSEC PER SEC);

/*
 *指定定时器指定时间内执行的处理
 */
 
dispatch_source_set_event handler(timer, ^{
    
    
	NSLog(@"wakeup!");
	/*
	 *取消 Dispatch Source
	 */
	dispatch_source_cancel(timer);
});
/*
 *指定取消 Dispatch Source 时的处理
 */
dispatch_source_set _cancel_handler(timer, ^{
    
    
	NSLog(@"canceled”);
	/*
	 *释放Dispatch Source(自身)
	 */
	dispatch release(timer);
});
/*
 *启动 Dispatch Source
 */
dispatch_resume(timer);

看了异步读取文件映像用的源代码和这个定时器用的源代码后,有没有注意到什么呢?实际上Dispatch Queue 没有“取消”这一概念。一旦将处理追加到Dispatch Queue 中,就没有方法可将该处理去除,也没有方法可在执行中取消该处理。编程人员要么在处理中导入取消这一概念,要么放弃取消,或者使用 NSOperationQueue 等其他方法。
Dispatch SourceDispatch Queue 不同,是可以取消的。而且取消时必须执行的处理可指定为回调用的 Block形式。因此使用 Dispatch Source 实现 XNU 内核中发生的事件处理要比直接使用 kqueue 实现更为简单。在必须使用 kqueue 的情况下希望大家还是使用 Dispatch Source,它比较简单。
通过讲解,大家应该已经理解了主要的 Dispatch Queue 以及次要的 Dispatch Source 了吧。

猜你喜欢

转载自blog.csdn.net/weixin_52192405/article/details/123398219
今日推荐