iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-上篇(自动引用计数)
iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-中篇(Blocks)
iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-下篇(GCD)
本章主要介绍从OS X Snow Leopard和iOS4开始引入的新多线程编程功能“Grand Central Dispatch”。它给多线程编程带来了什么呢?本章就通过GCD的实现带领大家了解这些内容。
目录
- 3.1 Grand Central Dispatch (GCD) 概要
- 3.1.1 什么是GCD
- 3.1.2 多线程编程
- 3.2 GCD的API
- 3.2.1 Dispatch Queue
- 3.2.2 dispatch_queue_create
- 3.2.3 Main Dispatch Queue/Global Dispatch Queue
- 3.2.4 dispatch_set_target_queue
- 3.2.5 dispatch_after
- 3.2.6 Dispatch Group
- 3.2.7 dispatch_barrier_async
- 3.2.8 dispatch_sync
- 3.2.9 dispatch_apply
- 3.2.10 dispatch_suspend / dispatch_resume
- 3.211 Dispatch Semaphore
- 3.2.12 dispatch_once
- 3.2.13 Dispatch I/O
- 3.3 GCD 实现
- 3.3.1 Dispatch Queue
- 3.3.2 Dispatch Source
- 附录A
- ARC、Blocks、GCD使用范例
3.1 Grand Central Dispatch (GCD) 概要
3.1.1 什么是GCD
Grand Central Dispatch (GCD)是一步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,由此可统一管理,也可执行任务,这样就比以前的线程更有效率。
也就是说,GCD用我们难以置信的非常简洁的记述方法,实现了极为复杂繁琐的多线程编程,可以说这是一项划时代的技术。下面是使用了GCD源代码的例子。
dispatch_async(queue,^) {
/*
长时间处理:例如AR用图像识别;例如数据库访问
*/
dispatch_async(dispatch_get_main_queue(),^ {
/*
只在主线程可以执行的处理:例如用户界面更新
*/
})
});
上面的就是在后台线程中执行长时间处理,处理结束时,主线程使用该处理结果的源代码。
这仅有一行的代码表示让处理在后台线程中执行。
dispatch_async(dispatch_get_main_queue(),^{
});
另外,大家看到脱字符号“^”就能发现,GCD使用了上一章讲到的“Blocks”,进一步简化了应用程序代码。
在导入GCD之前,Cocoa框架提供了NSObject类的performSelectorInBackground:wtihObject实例方法和performSelectorOnMainThread实例方法等简单的多线程编程技术。例如,可以改用performSelector系方法来实现前面使用了GCD的源代码看看。
/*
NSObject performSelectorInBackground:wtihObject:方法中
执行后台线程
*/
- (void)launchThreadByNSObject_perforSelectorInBackground_withObject {
[self performSelectorInBackground:@selector(doWork) withObject:nil];
}
// 后台线程处理方法
- (void)doWork {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
/*
*长时间处理
*例如AR用画像识别
* 例如数据库访问
*/
/*
*长时间处理结束,主线程使用其处理结果。
*/
[self performSelectorOnMainThread:@selector(doneWork)withObject:nil waitUntilDone:NO];
[pool drain];
}
// x主线程处理方法
- (void)doneWork {
// 只在主线程可以执行的处理:例如用户界面更新
}
performSelector系方法确实要比使用NSThread类进行多线程编程简单,但与之前使用GCD的源代码相比,结果一目了然。相比performSelector系方法,GCD更为简洁。如果使用GCD,不仅不必使用NSThread类或performSelector系方法这些过时的API,更可以通过GCD提供的系统线程级线程管理提高执行效率。
3.1.2 多线程编程
线程到底是什么呢?
一个CPU一次只能执行一个命令,不能执行某处分开的并列的两个命令,因此通过CPU执行的CPU命令列就好比一条无分叉的大道,其执行不会出现分歧。
这里所说的“一个CPU执行的CPU命令列为一条无分叉路径”即为“线程”。
现在一个物理的CPU芯片实际上有64个(64核)CPU,如果一个CPU核虚拟为两个CPU核工作,那么一台计算机上使用多个CPU核就是理所当然的事了。尽管如此,“一个CPU执行的CPU命令列为一条无分叉路径”仍然不变。
这种无分叉路径不止一条,存在有多条时即为“多线程”。在多线程中,1个CPU核执行多条不通过路径上的不同命令。
虽然CPU的相关技术有很多,其进步也令人眼花缭乱,但基本上1个CPU核一次能够执行的CPU命令始终为1.那么怎么样才能在多条路径中执行CPU命令列呢?
虽然CPU的相关技术有很多,其进步也令人眼花缭乱,但基本上1个CPU核一次能够执行 的CPU命令始终为1。那么怎样才能在多条路径中执行CPU命令列呢?
OS X和iOS的核心XNU内核在发生操作系统事件时(如毎隔一定时间,唤起系统调用等情况)会切换执行路径。执行中路径的状态,例如CPU的寄存器等信息保存到各自路径专用的内存块中,从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的 CPU命令列。这被称为“上下文切换”。
由于使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上去就好像1个CPU核能够并列地执行多个线程一样。而且在具有多个CPU核的情况下,就不是 “看上去像”了,而是真的提供了多个CPU核并行执行多个线程的技术。
这种利用多线程编程的技术就被称为“多线程编程”。
但是,多线程编程实际上是一种易发生各种问题的编程技术。比如多个线程更新相同的资源会导致数据的不一致(数据竞争)、停止等待事件的线程会导致多个线程相互持续等待(死锁)、 使用太多线程会消耗大量内存等。
尽管极易发生各种问题,也应当使用多线程编程。这是为什么呢?因为使用多线程编程可保证应用程序的响应性能。
应用程序在启动时,通过最先执行的线程,即“主线程”来描绘用户界面、处理触摸屏幕的事件等。如果在该主线程中进行长时间的处理,如AR用画像的识别或数据库访问,就会妨碍主线程的执行(阻塞)。在OSX和iOS的应用程序中,会妨碍主线程中被称为RunLoop的主循环的执行,从而导致不能更新用户界面、应用程序的画面长时间停滞等问题。
这就是长时间的处理不在主线程中执行而在其他线程中执行的原因。使用多线程编程,在执行长时间的处理时仍可保证用户界面的响应性能。
GCD大大简化了偏于复杂的多线程编程的源代码。下一节我们来看看GCD的API。
3.2 GCD的API
3.2.1 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-Fi
rst-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);
当变量queue为Serial Dispatch Queue时,因为要等待现在执行中的处理结束,所以首先执 行blk0, blk0执行结束后,接着执行blk1, blk1结束后再开始执行blk2,如此重复。同时执行的处理数只能有1个。即执行该源代码后,一定按照以下顺序进行处理。
blk0
blk1
blk2
blk3
blk4
blk5
blk6
blk7
当变量queue为Concurrent Dispatch Queue时,因为不用等待现在执行中的处理结束,所以首先执行blk0,不管blk0的执行是否结束,都开始执行后面的blk1,不管blk1的执行是否结束, 都开始执行后面的blk2,如此重复循环。
这样虽然不用等待处理结束,可以并行执行多个处理,但并行执行的处理数量取决于当前系统的状态。即iOS和OS X基于Dispatch Queue中的处理数、CPU核数以及CPU负荷等当前系统的状态来决定Concurrent Dispatch Queue中并行执行的处理数。所谓“并行执行”,就是使用多个线程同时执行多个处理。
iOS和OS X的核心一一XNU内核决定应当使用的线程数,并只生成所需的线程执行处理。 另外,当处理结束,应当执行的处理数减少时,XNU内核会结束不再需要的线程。XNU内核仅使用Concurrent Dispatch Queue便可完美地管理并行执行多个处理的线程。
像这样在Concurrent Dispatch Queue中执行处理时,执行顺序会根据处理内容和系统状态发生改变。它不同于执行顺序固定的Serial Dispatch Queue。在不能改变执行的处理顺序或不想并行执行多个处理时使用Serial Dispatch Queue。
虽然知道了有Serial Dispatch Queue和Concurrent Dispatch Queue这两种,但如何才能得到这些Dispatch Queue呢?方法有两种。
3.2.2 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 Queue和Concurrent 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 Queue 比 Concurrent 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.gccLMyConcurrentDispatchQueue", 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函数释放。
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(queue,^{
NSLog(@”block on myConcurrentDispatchQueue);
});
dispatch_release(myConcurrentDispatchQueue);
虽然Concurrent Dispatch Queue是使用多线程执行追加的处理,但像该例这样,在dispatch_async函数中追加Block到Concurrent Dispatch Queue,并立即通过dispatch_release函数进行释放是否可以呢?
该源代码完全没有问题。在dispatch_async函数中追加Block到Dispatch Queue,换言之,该 Block 通过 dispatch_retain 函数持有 Dispatch Queue。无论 Dispatch Queue 是 Serial 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 函数释放。
3.2.3 Main Dispatch Queue/Global Dispatch Queue
第二种方法是获取系统标准提供的Dispatch Queue。
实际上不用特意生成Dispatch Queue系统也会给我们提供几个。那就是Main Dispatch Queue 和 Global 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 Queue是所有应用程序都能够使用的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内核用于Global Dispatch Queue的线程并不能保证实时性,因此执行优先级只是大致的判断。例如在处理内容的执行可有可无时,使用后台优先级的Global Dispatch Queue 等,只能进行这种程度的区分。
另外,对于 Main Dispatch Queue 和 Global Dispatch Queue 执行 dispatch_retain 函数和 dispatch_release函数不会引起任何变化,也不会有任何问题。这也是获取并使用Global Dispatch Queue比生成、使用、释放Concurrent Dispatch Queue更轻松的原因。
以下列举使用了 Main Dispatch Queue 和 Global Dispatch Queue 的源代码:
//在默认优先级的Global Dispatch Queue中执行Block
dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//可并行执行的处理
//...
//在 Main Dispatch Queue 中执行 Block
dispatch_async(dispatch_get_main_queue(),^{
//只能在主线程中执行的处理
//...
});
});
3.2.4 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, 0);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);
指定要变更执行优先级的Dispatch Queue为dispatch_set_target_queue函数的第一个参数,指定与要使用的执行优先级相同优先级的Global Dispatch Queue为第二个参数(目标)。第一个 数如果指定系统提供的Main Dispatch Queue和Globa Dispatch Queue则不知道会出现什么状况, 因此这些均不可指定。
将 Dispatch Queue 指定为dispatch_set_target_queue 函数的参数,不仅可以变更 Dispatch Queue 的执行优先级,还可以作成Dispatch Queue的执行阶层。如果在多个Serial Dispatch Queue中用dispatch_set_target_queue函数指定目标为某一个Seria Dispatch Queue,那么原先本应并行执行的多个Serial Dispatch Queue,在目标Serial Dispatch Queue上只能同时执行一个处理。
在必须将不可并行执行的处理追加到多个Serial Dispatch Queue中时,如果使用dispatch_set_target_queue函数将目标指定为某一个Serial Dispatch Queue,即可防止处理并行执行。
3.2.5 dispatch_after
经常会有这样的情况:想在3秒后执行处理。可能不仅限于3秒,总之,这种想在指定时间后执行处理的情况,可使用dispatch_after函数来实现。
在3秒后将指定的Block追加到Main Dispatch Queue中的源代码如下:
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_after函数并不是在指定时间后执行处理,而只是在指定时间追加处理到Dispatch Queue。此源代码与在3秒后用dispatch_async函数追加Block到Main Dispatch 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(DISPATCHJTIME_NOW, 150ull * NSEC_PER_MSEC);
dispatch_walltime 函数由 POSIX 中使用的 struct timespec 类型的时间得到 dispatch_time_t 类型的值。dispatch_time函数通常用于计算相对时间,而dispatch_valltime函数用于计算绝对时间。 例如在dispatch_after函数中想指定2011年11月11日11时11分11秒这一绝对时间的情况,这可作为粗略的闹钟功能使用。
struct timespec类型的时间可以很轻松地通过NSDate类对象作成。可由NSDate类对象获取能传递给dispatch_after函数的dispatch_time_t类型的值。
3.2.6 Dispatch Group
在追加到Dispatch Queue中的多个处理全部结束后想执行结束处理,这种情况会经常出现。 只使用一个Serial Dispatch Queue时,只要将想执行的处理全部追加到该Serial Dispatch Queue 中并在城后追加结束处理,即可实现。但是在使用Concurrent Dispatch Queue时或同时使用多个 Dispatch Queue时,源代码就会变得颇为复杂。
在此种情况下使用Dispatch Group。例如下面源代码为:追加3个Block到Global Dispatch Queue,这些Block如果全部执行完毕,就会执行Main Dispatch Queue中结束处理用的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");});
dispatch_release(group};
该源代码的执行结果如下:
blk1
blk2
bl0
done
因为向Global Dispatch Queue即Concurrent Dispatch Queue追加处理,多个线程并行执行,所以追加处理的执行顺序不定。执行时会发生变化,但是此执行结果的done—定是最后输出的。
无论向什么样的Dispatch Queue中追加处理,使用Dispatch Group都可监视这些处理执行的结束。一旦检测到所有处理执行结束,就可将结束的处理追加到Dispatch Queue中。这就是使用 Dispatch Group 的原因。
首先 dispatch_group_create 函数生成 dispatch_group_t 类型的 Dispatch Group。如 dispatch_group_create函数名中所含的create所示,该Dispatch Group与Dispatch 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, 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_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, 1ull * 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函数可以简化源代码。
3.2.7 dispatch_barrier_async
在访问数据库或文件时,如前所述,使用Serial Dispatch Queue可避免数据竞争的问题。
写入处理确实不可与其他的写入处理以及包含读取处理的其他某些处理并行执行。但是如果读取处理只是与读取处理并行执行,那么多个并行执行就不会发生问题。
也就是说,为了高效率地进行访问,读取处理追加到Concurrent Dispatch Queue中,写入处理在任一个读取处理没有执行的状态下,追加到Serial Dispatch Queue中即可(在写入处理结束之前,读取处理不可执行)。
虽然利用Dispatch Group和dispatch_set_target_queue函数也可实现,但是源代码会很复杂。
GCD为我们提供了更为聪明的解决方法dispatch_barrier_async函数。该函数同dispatch_queue_create 函数生成的Concurrent Dispatch Queue —起使用。
如果像下面这样简单地在dispatch_async函数中加入写入处理,那么根据Concurrent Dispatch Queue的性质,就有可能在追加到写入处理前面的处理中读取到与期待不符的数据,还可能因非法访问导致应用程序异常结束。如果追加多个写入处理,则可能发生更多问题,比如数据竞争等。
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.ForBarrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_writing);
dispatch_async(queue, blk3_for_reading);
dispatch_async(queue, blk4_for_reading);
dispatch_release(queue);
因此我们要使用dispatch_barrier_async函数。dispatch_barrier_async函数会等待追加到 Concurrent Dispatch Queue上的并行执行的处理全部结束之后,再将指定的处理追加到该Concurrent Dispatch Queue中。然后在由dispatch_barrier_async函数追加的处理执行完毕后, Concurrent Dispatch Queue才恢复为一般的动作,追加到该Concurrent Dispatch Queue的处理又开始并行执行。
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.ForBarrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch__barrier_async(queue, blk2_for_writing);
dispatch_async(queue, blk3_for_reading);
dispatch_async(queue, blk4_for_reading);
dispatch_release(queue);
如上所示,使用方法非常简单。仅使用dispatch_barrier_async函数代替dispatch_async函数即可。
使用Concurrent Dispatch Queue和dispatch barrier async函数可实现高效率的数据库访问和文件访问。
3.2.8 dispatch_sync
dispatch_async函数的“async”意味着“非同步”(asynchronous),就是将指定的Block “非 同步”地追加到指定的Dispatch Queue中。dispatch async函数不做任何等待。
既然有“async”,当然也就有“sync”,即dispatch^sync函数。它意味着“同步” (synchronous),也就是将指定的Block “同步”追加到指定的Dispatch Queue中。在追加Block 结束之前,dispatch sync函数会一直等待。
如diSpatCh^gr〇up_wait函数说明所示(参考3.2.6节),“等待”意味着当前线程停止。
我们先假设这样一种情况:执行Main Dispatch Queue时,使用另外的线程Globa丨Dispatch Queue进行处理,处理结束后立即使用所得到的结果。在这种情况下就要使用dispatchjync函数。
dispatch_queue—t queue -
dispatch^get global^queue(DISPATCH QUEUE PRIORITY DEFAULT, 0);
dispatch一sync (queue, ^{
/* 处理 */
});
一旦调用dispatch_sync函数,那么在指定的处理执行结束之前,该函数不会返回。dispatch_sync函数可简化源代码,也可说是简易版的dispatch_group_wait函数。
正因为dispatch_sync函数使用简单,所以也容易引起问题,即死锁。
例如如果在主线程中执行以下源代码就会死锁。
dispatc_queue_t queue(dispatch_get_main_queue();
dispatc_sync(queue, ^{
NSLog(@"Hello?);
});
该源代码在Main Dispatch Queue即主线程中执行指定的Block,并等待其执行结束。而其实在主线程中正在执行这些源代码,所以无法执行追加到Main Dispatch Queue的Block。下面例子也一样。
dispatc_queue_t queue = dispatch_get_main_queue();
dispatc_async(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时,稍有不慎就会导致程序死锁,我想大家都不希望发生这种情况吧。
3.2.9 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");
例如,该源代码的执行结果为:4 1 0 3 5 2 6 8 9 7 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 objectAtlndex: 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 objectAtlndex:index]);
});
//dispatch_apply函数中的处理全部执行结束
//在Main Dispatch Queue中非同步执行
dispatch_async (dispatch_get_main_queue () , ^{
//在Main Dispatch Queue中执行处理 *用户界面更新等
NSLog (@"done" );
});
});
3.2.10 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中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。
3.211 Dispatch Semaphore
如前所述,当并行执行的处理更新数据时,会产生数据不一致的情况,有时应用程序还会异常结束。虽然使用Serial Dispatch Queue和dispatch_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 numberWithlnt: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 Queue和Dispatch 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 SemaphoreDispatch 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 numberWithlnt:i]];
//排他控制处理结束,所以通过dispatch_semaphore_signal函数 将Dispatch Semaphore 的计数值加1。如果有通过dispatch_semaphore_wait函数,等待Dispatch Semaphore的计数值增加的线程,就由最先等待的线程执行。
dispatch_semaphore_signal(semaphore );
});
}
//如果使用结束,需要如以下这样释放Dispatch Semaphore
//dispatch release(semaphore);
在没有Serial Dispatch Queue和dispatch_barrier_async函数那么大粒度且一部分处理需要进行排他控制的情况下,Dispatch Semaphore便可发挥威力。
3.2.12 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函数初始化就不必担心这样的问题。这就是所说的单例模式,在生成单例对象时使用。
3.2.13 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 Data可更为简单地进行结合和分割。
以下为苹果中使用Dispatch I/O和Dispatch Data的例子。(待补充)
dispatch_io_create 函数生成Dispatch I/O,并指定发生错误时用来执行处理的Block,以及执行该Block的Dispatch Queue。dispatch_io_set_low_water函数设定一次读取的大小(分割大小),dispatch_io_read函数使用Global Dispatch Queue开始并列读取。每当各个分割的文件块读取结束时,将含有文件 数据的Dispatch Data传递给dispatch_io_read函数指定的读取结束时回调用的Block。回调用的 Block分析传递过来的Dispatch Data并进行结合处理。
如果想提高文件读取速度,可以尝试使用Dispatch I/O。
3.3 GCD 实现
3.3.1 Dispatch Queue
GCD的Dispatch Queue非常方便,那么它究竟是如何实现的呢?
- 用于处理追加的Block的C语言层实现的FIFO队列;
- Atomic函数中实现的用于排他控制的轻量级信号;
- 用于管理线程的C语言层实现的一些容器;
不难想象,GCD的实现需要使用以上这些工具。但是,如果仅用这些内容便可实现,那么就不需要内核级的实现了。
甚至有人会想,只要努力编写线程管理的代码,就根本用不到GCD。真的是这样吗?
我们先来回顾一下苹果的官方说明。
通常,应用程序中编写的线程管理用的代码要在系统级实现。
实际上正如这句话所说,在系统级即iOS和OS X的核心XNU内核级上实现。
因此,无论编程人员如何努力编写管理线程的代码,在性能方面也不可能胜过XNU内核级所实现的GCD。
使用GCD要比使用pthreads和NSThread这些一般的多线程编程API更好。并且,如果使用GCD就不必编写为操作线程反复出现的类似的源代码(这被称为固定源代码片断)而可以在线程中集中实现处理内容,真的是好处多多。我们尽量多使用GCD或者使用了Cocoa框架GCD的 NSOperationQueue 类等 API。
编程人员所使用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 Queue的各种优先级的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) (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_register 和 workq_open系统调用,在初始化XNU内核的workqueue之后获取workqueue信息。
XNU内核持有4种workqueue。
• WORKQUEUE_HIGH_PRIOQUEUE
• WORKQUEUE_DEFAULT_PRIOQUEUE
• WORKQUEUE_LOW_PRIOQUEUE
• WORKQUEUE_BG_PRIOQUEUE
以上为4种执行优先级的workqueue。该执行优先级与Global Dispatch Queue的4种执行优先级相同。
下面看一下Dispatch Queue中执行Block的过程。当在Global Dispatch Queue中执行Block时,libdispatch 从 Global Dispatch Queue 自身的 FIFO 队列中取出 Dispatch Continuation,调用 pthread_workqueue_additem_np 函数。将该 Global Dispatch Queue 自身、符合其优先级的 workqueue 信息以及为执行Dispatch Continuation的回调函数等传递给参数。
thread_workqueue_additem_np 函数使用 workq_kemretum 系统调用,通知 workqueue 增加应当执行的项目。根据该通知,XNU内核基于系统状态判断是否要生成线程。如果是Overcommit 优先级的Global Dispatch Queue, workqueue则始终生成线程。
该线程虽然与iOS和OS X中通常使用的线程大致相同,但是有一部分pthread API不能使用。详细信息可参考苹果的官方文档《并列编程指南》的“与POSIX线程的互换性” 一节。
另外,因为workqueue生成的线程在实现用于workqueue的线程计划表中运行,所以与一般线程的上下文切换不同。这里也隐藏着使用GCD的原因。
workqueue的线程执行pthread_workqueue函数,该函数调用libdispatch的回调函数。在该回调闲数中执行加入到Dispatch Continuation的Block。
Block执行结束后,进行通知Dispatch Group结束、释放Dispatch Continuation等处理,幵始准备执行加入到Global Dispatch Queue中的下一个Block。
以上就是Dispatch Queue执行的大概过程。
由此可知,在编程人员管理的线程中,想发挥出匹敌GCD的性能是不可能的。
3.3.2 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_ DATA_PROC | 检测到与进程相关的事件 |
DISPATCH_SOURCE_TYPE_ DATA_READ | 可读取文件映像 |
DISPATCH_SOURCE_TYPE_ DATA_SIGNAL | 接收信号 |
DISPATCH_SOURCE_TYPE_ DATA_TIMER | 定时器 |
DISPATCH_SOURCE_TYPE_ DATA_VNODE | 文件系统有变更 |
DISPATCH_SOURCE_TYPE_ DATA_WRITE | 可写入文件映像 |
事件发生时,在指定的Dispatch Queue中可执行事件的处理。
下面我们使用DISPATCH_SOURCE_TYPE_READ,异步读取文件映像。
__block size_t total = 0;
size_t size =要读取的字节数
char *buff = (char * ) malloc (size);
//设定为异步映像
fcntl (sockfd, F_SETFL, 0_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_SOURCEJTYPEJTIMER,作成 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, 15ull*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 Source与Dispatch Queue不同,是可以取消的。而且取消时必须执行的处理可指定为回调用的Block形式。因此使用Dispatch Source实现XNU内核中发生的事件处理要比直接使用kqueue实现更为简单。在必须使用kqueue的情况下希望大家还是使用Dispatch Source,它比较简单。
通过讲解,大家应该己经理解了主要的Dispatch Queue以及次要的Dispatch Source 了吧。
附录A:ARC、Blocks、GCD使用范例
我们实际使用一下ARC、Blocks和GCD。实现从指定的URL下载数据,在另外的线程中解析该数据并在主线程中使用其解析结果的源代码如下。代码中穿插了注释加以解说。
NSString *url = "http://images.apple.com/jp/iphone/features/includes/camera-gallery/03-20100607.jpg"
//在主线程中,从指定的URL开始异步网络下载
[ASyncURLConnection request:url completeBlock:^(NSData *data ) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
//在Global Dispatch Queue中对下载的数据进行解析处理。不妨碍主线程可长时间处理。
dispatch_async ( dispatch_get_main_queue(),^{
//在Main Dispatch Queue中使用解析结果。对用户界面进行反映处理
});
} errorBlock: M NSError *error } {
//发生错误
NSLog ( @"error %@",error);
}];
为了不妨碍主线程的运行,在另外的线程中解析下载的数据。实现数据解析的就是通过dispatch_get_global_queue 函数得到的一般优先级的 Global Dispatch Queue 和在 Global Dispatch Queue中执行解析处理的diSpatch_async函数。解析处理后为了反映到用户界面,需要在主线程中进行用户界面的更新。通过dispatch_get_main_queue函数得到的Main Dispatch Queue和在 Main Dispatch Queue中使该处理执行的dispatch_async函数实现了此处理。
那么为了不妨碍主线程的运行,网络下载处理也是使用GCD的线程更好吗?答案是否定的。网络编程强烈推荐使用异步API。请大家看一下WWDC 2010的以下两个议题。
- WWDC 2010 议题 207 - Network Apps for iPhone OS,Part 1
- WWDC 2010 议题 208 - Network Apps for iPhone OS,Part 2
这是关于网络编程的议题,对于网络编程可以断言“线程是魔鬼”(Threads Are Evil™)。如果在网络编程中使用线程,就很可能会产生人量使用线程的倾向,会引发很多问题。例如每个连接都使用线程,很快就会用尽线程栈内存等。因为Cocoa框架提供了用于异步网络通信的API, 所以在网络编程中不可使用线程。务必使用用于异步网络通信的API。
前面源代码中使用的用于网络通信的类ASyncURLConnection,将Foundation框架中用于异步通信的类NSURLConnection作为基类。下面我们来看看它的实现。
▼ ASyncURLConnection.h
#import <Foundation/Foundation.h>
//typedef Block类型变置 提高源代码的可读性
typedef void (^completeBlock_t) (NSData *data);
typedef void (^errorBlock_t) (NSError *error);
@interface ASyncURLConnection : NSURLConnection {
//由于ARC有效,所以以下的 没有显式附加所有权修饰符的变置 全部为附有strong修饰符的变置。
NSMutableData *data_;
completeBlock_t completeBlock_;
errorBlock_t errorBlock_;
}
//为提高源代码的可读性 使用typedef的Block类型变量作为参数
+ (id)request: (NSString *)requestUrl completeBlock: (completeBlock_t) completeBlock errorBlock: (errorBlock_t )errorBlock;
- (id)initWithRequest: (NSString *) requestUrl completeBlock: (completeBlock_t )completeBlock errorBlock:(errorBlock_t)errorBlock;
@end
▼ ASyncURLConnection.m
#import "ASyncURLConnection.h"
@implemention ASyncURLConnection
+ (id)request: (NSString *)requestUrl completeBlock: (completeBlock_t) completeBlock errorBlock: (errorBlock_t )errorBlock {
//ARC无效时,如以下这样应当用autorelease类方法返回
/*
id obj = [[self alloc] initWithRequest:requestUrl completeBlock:completeBlock errorBlock:errorBlock];
return [obj autorelease];
*/
//因为此方法的方法名不是以alloc/new/copy/mutableCopy开头的,所以ARC有效时,自动地返回注册到autoreleasepool中的对象。
return [[self alloc] initWithRequest:requestUrl completeBlock:completeBlock errorBlock:rerrorBlock];
}
- (id)initWithRequest: (NSString *) requestUrl completeBlock: (completeBlock_t )completeBlock errorBlock:(errorBlock_t)errorBlock {
NSURL *url = [NSURL URLWithString:requestUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
if ((self = [super initWithRequest:request delegate:self startlmmediately:NO])) {
data_ = [[NSMutableData alloc] init];
//为了在之后的代码中安全地使用 *传递到此方法中的Block,调用copy实例方法确保Block被配置在堆上
completeBlock_ = [completeBlock copy];
errorBlock_ = [errorBlock copy];
[self start];
}
//生成的NSMutableData类对象和copy的Block由附有 strong修饰符的成员变置强引用,处于被持有的状态。因此如果该对象被废弃,附有_strong修饰符成员变置的强引用也随之失效,NSMutableData类对象和Block自动地释放。由此dealloc实例方法不用显式实现。
return self;
}
- (void)connection: ( NSURLConnection *) connection didReceiveResponse: (NSURLResponse *)response {
[data_ setLength:0];
}
- (void)connection: (NSURLConnection *) connection didReceiveData: (NSData *) data {
[data_ appendData:data];
}
- (void)connectionDidFinishLoading: (NSURLConnection *) connection {
//下载成功时调用用于回调的Block
completeBlock_ ( data_ );
}
- (void)connection: (NSURLConnection *) connection didFailWithError: (NSError *)error {
//发生错误时调用用于回调的Block
errorBlock一(error );
}
@end
NSURLConnection类在下载结束时和发生错误时调用delegate指定对象的方法。ASyncURLConnection类继承NSURLConnection类并能够指定Block下载结束时和发生错误时回调。这可使源代码更为简化。
另外赋值给该类成员变量中的用于下载数据的NSMutableData类对象和用于回调的Block由ARC进行适当的内存管理。使用附有_strong修饰符的成员变量,就不用显式地调用retain方法和release方法(ARC本来也不可调用),另外也不用实现dealloc实例方法了。
通过如此简单的源代码,也可以充分地感受到ARC、Blocks以及Grand Central Dispatch的威力。