什么是GCD?node
Grand Central Dispatch或者GCD,是一套低层API,提供了一种新的方法来进行并发程序编写。从基本功能上讲,GCD有点像 NSOperationQueue,他们都容许程序将任务切分为多个单一任务而后提交至工做队列来并发地或者串行地执行。GCD比之 NSOpertionQueue更底层更高效,而且它不是Cocoa框架的一部分。编程
除了代码的平行执行能力,GCD还提供高度集成的事件控制系统。能够设置句柄来响应文件描述符、mach ports(Mach port 用于 OS X上的进程间通信)、进程、计时器、信号、用户生成事件。这些句柄经过GCD来并发执行。数组
GCD的API很大程度上基于block,固然,GCD也能够脱离block来使用,好比使用传统c机制提供函数指针和上下文指针。实践证实,当配合block使用时,GCD很是简单易用且能发挥其最大能力。安全
你能够在Mac上敲命令“man dispatch”来获取GCD的文档。多线程
为什么使用?并发
GCD提供不少超越传统多线程编程的优点:app
Dispatch Objects框架
尽管GCD是纯c语言的,但它被组建成面向对象的风格。GCD对象被称为dispatch object。Dispatch object像Cocoa对象同样是引用计数的。使用dispatch_release和dispatch_retain函数来操做dispatch object的引用计数来进行内存管理。但注意不像Cocoa对象,dispatch object并不参与垃圾回收系统,因此即便开启了GC,你也必须手动管理GCD对象的内存。异步
Dispatch queues 和 dispatch sources(后面会介绍到)能够被挂起和恢复,能够有一个相关联的任意上下文指针,能够有一个相关联的任务完成触发函数。能够查阅“man dispatch_object”来获取这些功能的更多信息。socket
Dispatch Queues
GCD的基本概念就是dispatch queue。dispatch queue是一个对象,它能够接受任务,并将任务以先到先执行的顺序来执行。dispatch queue能够是并发的或串行的。并发任务会像NSOperationQueue那样基于系统负载来合适地并发进行,串行队列同一时间只执行单一任务。
GCD中有三种队列类型:
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
dispatch_queue_create
建立的队列. 这些队列是串行的。正由于如此,它们能够用来完成同步机制, 有点像传统线程中的mutex。建立队列
要使用用户队列,咱们首先得建立一个。调用函数dispatch_queue_create就好了。函数的第一个参数是一个标签,这纯是为了debug。 Apple建议咱们使用倒置域名来命名队列,好比“com.dreamingwish.subsystem.task”。这些名字会在崩溃日志中被显示出 来,也能够被调试器调用,这在调试中会颇有用。第二个参数目前还不支持,传入NULL就好了。
提交 Job
向一个队列提交Job很简单:调用dispatch_async函数,传入一个队列和一个block。队列会在轮到这个block执行时执行这个block的代码。下面的例子是一个在后台执行一个巨长的任务:
dispatch_async
函数会当即返回, block会在后台异步执行。
固然,一般,任务完成时简单地NSLog个消息不是个事儿。在典型的Cocoa程序中,你颇有可能但愿在任务完成时更新界面,这就意味着须要在主线程中执 行一些代码。你能够简单地完成这个任务——使用嵌套的dispatch,在外层中执行后台任务,在内层中将任务dispatch到main queue:
还有一个函数叫dispatch_sync,它干的事儿和dispatch_async相同,可是它会等待block中的代码执行完成并返回。结合 __block类型修饰符,能够用来从执行中的block获取一个值。例如,你可能有一段代码在后台执行,而它须要从界面控制层获取一个值。那么你可使 用dispatch_sync简单办到:
咱们还可使用更好的方法来完成这件事——使用更“异步”的风格。不一样于取界面层的值时要阻塞后台线程,你可使用嵌套的block来停止后台线程,而后从主线程中获取值,而后再将后期处理提交至后台线程:
dispatch_queue_t bgQueue = myQueue; dispatch_async(dispatch_get_main_queue(), ^{ NSString *stringValue = [[[textField stringValue] copy] autorelease]; dispatch_async(bgQueue, ^{ // use stringValue in the background now }); });
取决于你的需求,myQueue能够是用户队列也可使全局队列。
再也不使用锁(Lock)
用户队列能够用于替代锁来完成同步机制。在传统多线程编程中,你可能有一个对象要被多个线程使用,你须要一个锁来保护这个对象:
NSLock *lock;
访问代码会像这样:
使用GCD,可使用queue来替代:
dispatch_queue_t queue;
要用于同步机制,queue必须是一个用户队列,而非全局队列,因此使用usingdispatch_queue_create
初始化一个。而后能够用dispatch_async
或者 dispatch_sync
将共享数据的访问代码封装起来:
值得注意的是dispatch queue是很是轻量级的,因此你能够大用特用,就像你之前使用lock同样。
如今你可能要问:“这样很好,可是有意思吗?我就是换了点代码办到了同一件事儿。”
实际上,使用GCD途径有几个好处:
-setSomething:是怎么使用dispatch_async的。调用
-setSomething:会当即返回,而后这一大堆工做会在后台执行。若是updateSomethingCaches是一个很费时费力的任务,且调用者将要进行一项处理器高负荷任务,那么这样作会很棒。总结
如今你已经知道了GCD的基本概念、怎样建立dispatch queue、怎样提交Job至dispatch queue以及怎样将队列用做线程同步。接下来我会向你展现如何使用GCD来编写平行执行代码来充分利用多核系统的性能^ ^。我还会讨论GCD更深层的东西,包括事件系统和queue targeting。
概念
为了在单一进程中充分发挥多核的优点,咱们有必要使用多线程技术(咱们不必去提多进程,这玩意儿和GCD不要紧)。在低层,GCD全局dispatch queue仅仅是工做线程池的抽象。这些队列中的Block一旦可用,就会被dispatch到工做线程中。提交至用户队列的Block最终也会经过全局 队列进入相同的工做线程池(除非你的用户队列的目标是主线程,可是为了提升运行速度,咱们毫不会这么干)。
有两种途径来经过GCD“榨取”多核心系统的性能:将单一任务或者一组相关任务并发至全局队列中运算;将多个不相关的任务或者关联不紧密的任务并发至用户队列中运算;
全局队列
设想下面的循环:
1
2
|
for
(
id
obj in array)
[
self
doSomethingIntensiveWith
:obj];
|
假定 -doSomethingIntensiveWith:
是线程安全的且能够同时执行多个.一个array一般包含多个元素,这样的话,咱们能够很简单地使用GCD来平行运算:
1
2
3
4
5
|
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0
);
for
(
id
obj in array)
dispatch_async(queue, ^{
[
self
doSomethingIntensiveWith
:obj];
});
|
如此简单,咱们已经在多核心上运行这段代码了。
固然这段代码并不完美。有时候咱们有一段代码要像这样操做一个数组,可是在操做完成后,咱们还须要对操做结果进行其余操做:
1
2
3
|
for
(
id
obj in array)
[
self
doSomethingIntensiveWith
:obj];
[
self
doSomethingWith
:array];
|
这时候使用GCD的 dispatch_async
就悲剧了.咱们还不能简单地使用dispatch_sync来解决这个问题
, 由于这将致使每一个迭代器阻塞,就彻底破坏了平行计算。
解决这个问题的一种方法是使用dispatch group。一个dispatch group能够用来将多个block组成一组以监测这些Block所有完成或者等待所有完成时发出的消息。使用函数 dispatch_group_create来建立,而后使用函数dispatch_group_async来将block提交至一个dispatch queue,同时将它们添加至一个组。因此咱们如今能够从新编码:
1
2
3
4
5
6
7
8
9
10
|
dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0
);
dispatch_group_t group = dispatch_group_create();
for
(
id
obj in array)
dispatch_group_async(group, queue, ^{
[
self
doSomethingIntensiveWith
:obj];
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);
[
self
doSomethingWith
:array];
|
若是这些工做能够异步执行,那么咱们能够更风骚一点,将函数-doSomethingWith:放在后台执行。咱们使用dispatch_group_async函数创建一个block在组完成后执行:
1
2
3
4
5
6
7
8
9
10
|
dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0
);
dispatch_group_t group = dispatch_group_create();
for
(
id
obj in array)
dispatch_group_async(group, queue, ^{
[
self
doSomethingIntensiveWith
:obj];
});
dispatch_group_notify(group, queue, ^{
[
self
doSomethingWith
:array];
});
dispatch_release(group);
|
不只全部数组元素都会被平行操做,后续的操做也会异步执行,而且这些异步运算都会将程序的其余部分的负载考虑在内。注意若是-doSomethingWith:须要在主线程中执行,好比操做GUI,那么咱们只要将main queue而非全局队列传给dispatch_group_notify函数就好了。
对于同步执行,GCD提供了一个简化方法叫作dispatch_apply。这个函数调用单一block屡次,并平行运算,而后等待全部运算结束,就像咱们想要的那样:
1
2
3
4
5
|
dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0
);
dispatch_apply([array
count
], queue, ^(size_t index){
[
self
doSomethingIntensiveWith
:[array
objectAtIndex
:index]];
});
[
self
doSomethingWith
:array];
|
这很棒,可是异步咋办?dispatch_apply函数但是没有异步版本的。可是咱们使用的但是一个为异步而生的API啊!因此咱们只要用dispatch_async函数将全部代码推到后台就好了:
1
2
3
4
5
6
7
|
dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0
);
dispatch_async(queue, ^{
dispatch_apply([array
count
], queue, ^(size_t index){
[
self
doSomethingIntensiveWith
:[array
objectAtIndex
:index]];
});
[
self
doSomethingWith
:array];
});
|
简单的要死!
这种方法的关键在于肯定咱们的代码是在一次对不一样的数据片断进行类似的操做。若是你肯定你的任务是线程安全的(不在本篇讨论范围内)那么你可使用GCD来重写你的循环了,更平行更风骚。
要看到性能提高,你还得进行一大堆工做。比之线程,GCD是轻量和低负载的,可是将block提交至queue仍是很消耗资源的——block须要被拷贝 和入队,同时适当的工做线程须要被通知。不要将一张图片的每一个像素做为一个block提交至队列,GCD的优势就半途夭折了。若是你不肯定,那么请进行试 验。将程序平行计算化是一种优化措施,在修改代码以前你必须再三思索,肯定修改是有益的(还有确保你修改了正确的地方)。
Subsystem并发运算
前面的章节咱们讨论了在程序的单个subsystem中发挥多核心的优点。下来咱们要跨越多个子系统。
例如,设想一个程序要打开一个包含meta信息的文档。文档数据自己须要解析并转换至模型对象来显示,meta信息也须要解析和转换。可是,文档数据和 meta信息不须要交互。咱们能够为文档和meta各建立一个dispatch queue,而后并发执行。文档和meta的解析代码都会各自串行执行,从而不用考虑线程安全(只要没有文档和meta之间共享的数据),可是它们仍是并 发执行的。
一旦文档打开了,程序须要响应用户操做。例如,可能须要进行拼写检查、代码高亮、字数统计、自动保存或者其余什么。若是每一个任务都被实现为在不一样的 dispatch queue中执行,那么这些任务会并发执行,并各自将其余任务的运算考虑在内(respect to each other),从而省去了多线程编程的麻烦。
使用dispatch source(下次我会讲到),咱们可让GCD将事件直接传递给用户队列。例如,程序中监视socket链接的代码能够被置于它本身的dispatch queue中,这样它会异步执行,而且执行时会将程序其余部分的运算考虑在内。另外,若是使用用户队列的话,这个模块会串行执行,简化程序。
结论
咱们讨论了如何使用GCD来提高程序性能以及发挥多核系统的优点。尽管咱们须要比较谨慎地编写并发程序,GCD仍是使得咱们能更简单地发挥系统的可用计算资源。
下一篇中,咱们将讨论dispatch source,也就是GCD的监视内部、外部事件的机制。
何为Dispatch Sources
简单来讲,dispatch source是一个监视某些类型事件的对象。当这些事件发生时,它自动将一个block放入一个dispatch queue的执行例程中。
说的貌似有点不清不楚。咱们到底讨论哪些事件类型?
下面是GCD 10.6.0版本支持的事件:
这是一堆颇有用的东西,它支持全部kqueue所支持的事件(kqueue是什么?见http://en.wikipedia.org/wiki/Kqueue)以及mach(mach是什么?见http://en.wikipedia.org/wiki/Mach_(kernel))端口、内建计时器支持(这样咱们就不用使用超时参数来建立本身的计时器)和用户事件。
用户事件
这些事件里面多数均可以从名字中看出含义,可是你可能想知道啥叫用户事件。简单地说,这种事件是由你调用dispatch_source_merge_data函数来向本身发出的信号。
这个名字对于一个发出事件信号的函数来讲,太怪异了。这个名字的来由是GCD会在事件句柄被执行以前自动将多个事件进行联结。你能够将数据“拼接”至 dispatch source中任意次,而且若是dispatch queue在这期间繁忙的话,GCD只会调用该句柄一次(不要以为这样会有问题,看完下面的内容你就明白了)。
用户事件有两种: DISPATCH_SOURCE_TYPE_DATA_ADD
和 DISPATCH_SOURCE_TYPE_DATA_OR
.用户事件源有个 unsigned long data
属性,咱们将一个 unsigned long
传入 dispatch_source_merge_data
。当使用 _ADD
版本时,事件在联结时会把这些数字相加。当使用 _OR
版本时,事件在联结时会把这些数字逻辑与运算。当事件句柄执行时,咱们可使用dispatch_source_get_data函数访问当前值,而后这个值会被重置为0。
让我假设一种状况。假设一些异步执行的代码会更新一个进度条。由于主线程只不过是GCD的另外一个dispatch queue而已,因此咱们能够将GUI更新工做push到主线程中。然而,这些事件可能会有一大堆,咱们不想对GUI进行频繁而累赘的更新,理想的状况是 当主线程繁忙时将全部的改变联结起来。
用dispatch source就完美了,使用DISPATCH_SOURCE_TYPE_DATA_ADD,咱们能够将工做拼接起来,而后主线程能够知道从上一次处理完事件到如今一共发生了多少改变,而后将这一整段改变一次更新至进度条。
啥也不说了,上代码:
(对于这段代码,我很想说点什么,我第一次用dispatch source时,我纠结了好久好久,真让人蛋疼:Dispatch source启动时默认状态是挂起的,咱们建立完毕以后得主动恢复,不然事件不会被传递,也不会被执行)
假设你已经将进度条的min/max值设置好了,那么这段代码就完美了。数据会被并发处理。当每一段数据完成后,会通知dispatch source并将dispatch source data加1,这样咱们就认为一个单元的工做完成了。事件句柄根据已完成的工做单元来更新进度条。若主线程比较空闲而且这些工做单元进行的比较慢,那么事 件句柄会在每一个工做单元完成的时候被调用,实时更新。若是主线程忙于其余工做,或者工做单元完成速度很快,那么完成事件会被联结起来,致使进度条只在主线 程变得可用时才被更新,而且一次将积累的改变动新至GUI。
如今你可能会想,听起来却是不错,可是要是我不想让事件被联结呢?有时候你可能想让每一次信号都会引发响应,什么后台的智能玩意儿通通不要。啊。。其实很 简单的,别把本身绕进去了。若是你想让每个信号都获得响应,那使用dispatch_async函数不就好了。实际上,使用的dispatch source而不使用dispatch_async的惟一缘由就是利用联结的优点。
内建事件
上面就是怎样使用用户事件,那么内建事件呢?看看下面这个例子,用GCD读取标准输入:
简单的要死!由于咱们使用的是全局队列,句柄自动在后台执行,与程序的其余部分并行,这意味着对这种状况的提速:事件进入程序时,程序正在处理其余事务。
这是标准的UNIX方式来处理事务的好处,不用去写loop。若是使用经典的 read
调用,咱们还得万分留神,由于返回的数据可能比请求的少,还得忍受无厘头的“errors”,好比 EINTR
(系统调用中断)。使用GCD,咱们啥都不用管,就从这些蛋疼的状况里解脱了。若是咱们在文件描述符中留下了未读取的数据,GCD会再次调用咱们的句柄。
对于标准输入,这没什么问题,可是对于其余文件描述符,咱们必须考虑在完成读写以后怎样清除描述符。对于dispatch source还处于活跃状态时,咱们决不能关闭描述符。若是另外一个文件描述符被建立了(多是另外一个线程建立的)而且新的描述符恰好被分配了相同的数字, 那么你的dispatch source可能会在不该该的时候忽然进入读写状态。de这个bug可不是什么好玩的事儿。
适当的清除方式是使用 dispatch_source_set_cancel_handler
,并传入一个block来关闭文件描述符。而后咱们使用 dispatch_source_cancel
来取消dispatch source,使得句柄被调用,而后文件描述符被关闭。
使用其余dispatch source类型也差很少。总的来讲,你提供一个source(mach port、文件描述符、进程ID等等)的区分符来做为diapatch source的句柄。mask参数一般不会被使用,可是对于 DISPATCH_SOURCE_TYPE_PROC
来讲mask指的是咱们想要接受哪种进程事件。而后咱们提供一个句柄,而后恢复这个source(前面我加粗字体所说的,得先恢复),搞定。dispatch source也提供一个特定于source的data,咱们使用 dispatch_source_get_data
函数来访问它。例如,文件描述符会给出大体可用的字节数。进程source会给出上次调用以后发生的事件的mask。具体每种source给出的data的含义,看man page吧。
计时器
计时器事件稍有不一样。它们不使用handle/mask参数,计时器事件使用另一个函数 dispatch_source_set_timer
来配置计时器。这个函数使用三个参数来控制计时器触发:
start
参数控制计时器第一次触发的时刻。参数类型是 dispatch_time_t
,这是一个opaque类型,咱们不能直接操做它。咱们得须要dispatch_time
和 dispatch_walltime
函数来建立它们。另外,常量 DISPATCH_TIME_NOW
和 DISPATCH_TIME_FOREVER
一般颇有用。
interval
参数没什么好解释的。
leeway
参数比较有意思。这个参数告诉系统咱们须要计时器触发的精准程度。全部的计时器都不会保证100%精准,这个参 数用来告诉系统你但愿系统保证精准的努力程度。若是你但愿一个计时器没五秒触发一次,而且越准越好,那么你传递0为参数。另外,若是是一个周期性任务,比 如检查email,那么你会但愿每十分钟检查一次,可是不用那么精准。因此你能够传入60,告诉系统60秒的偏差是可接受的。
这样有什么意义呢?简单来讲,就是下降资源消耗。若是系统可让cpu休息足够长的时间,并在每次醒来的时候执行一个任务集合,而不是不断的醒来睡去以执 行任务,那么系统会更高效。若是传入一个比较大的leeway给你的计时器,意味着你容许系统拖延你的计时器来将计时器任务与其余任务联合起来一块儿执行。
总结
如今你知道怎样使用GCD的dispatch source功能来监视文件描述符、计时器、联结的用户事件以及其余相似的行为。因为dispatch source彻底与dispatch queue相集成,因此你可使用任意的dispatch queue。你能够将一个dispatch source的句柄在主线程中执行、在全局队列中并发执行、或者在用户队列中串行执行(执行时会将程序的其余模块的运算考虑在内)。
下一篇我会讨论如何对dispatch queue进行挂起、恢复、重定目标操做;如何使用dispatch semaphore;如何使用GCD的一次性初始化功能。
Dispatch Queue挂起
dispatch queue能够被挂起和恢复。使用 dispatch_suspend
函数来挂起,使用 dispatch_resume
函数来恢复。这两个函数的行为是如你所愿的。另外,这两个函数也能够用于dispatch source。
一个要注意的地方是,dispatch queue的挂起是block粒度的。换句话说,挂起一个queue并不会将当前正在执行的block挂起。它会容许当前执行的block执行完毕,而后后续的block再也不会被执行,直至queue被恢复。
还有一个注意点:从man页上得来的:若是你挂起了一个queue或者source,那么销毁它以前,必须先对其进行恢复。
Dispatch Queue目标指定
全部的用户队列都有一个目标队列概念。从本质上讲,一个用户队列其实是不执行任何任务的,可是它会将任务传递给它的目标队列来执行。一般,目标队列是默认优先级的全局队列。
用户队列的目标队列能够用函数 dispatch_set_target_queue
来 修改。咱们能够将任意dispatch queue传递给这个函数,甚至能够是另外一个用户队列,只要别构成循环就行。这个函数能够用来设定用户队列的优先级。好比咱们能够将用户队列的目标队列设 定为低优先级的全局队列,那么咱们的用户队列中的任务都会以低优先级执行。高优先级也是同样道理。
有一个用途,是将用户队列的目标定为main queue。这会致使全部提交到该用户队列的block在主线程中执行。这样作来替代直接在主线程中执行代码的好处在于,咱们的用户队列能够单独地被挂起和恢复,还能够被重定目标至一个全局队列,而后全部的block会变成在全局队列上执行(只要你确保你的代码离开主线程不会有问题)。
还有一个用途,是将一个用户队列的目标队列指定为另外一个用户队列。这样作能够强制多个队列相互协调地串行执行,这样足以构建一组队列,经过挂起和暂停那个 目标队列,咱们能够挂起和暂停整个组。想象这样一个程序:它扫描一组目录而且加载目录中的内容。为了不磁盘竞争,咱们要肯定在同一个物理磁盘上同时只有 一个文件加载任务在执行。而但愿能够同时从不一样的物理磁盘上读取多个文件。要实现这个,咱们要作的就是建立一个dispatch queue结构,该结构为磁盘结构的镜像。
首先,咱们会扫描系统并找到各个磁盘,为每一个磁盘建立一个用户队列。而后扫描文件系统,并为每一个文件系统建立一个用户队列,将这些用户队列的目标队列指向 合适的磁盘用户队列。最后,每一个目录扫描器有本身的队列,其目标队列指向目录所在的文件系统的队列。目录扫描器枚举本身的目录并为每一个文件向本身的队列提 交一个block。因为整个系统的创建方式,就使得每一个物理磁盘被串行访问,而多个物理磁盘被并行访问。除了队列初始化过程,咱们根本不须要手动干预什么 东西。
信号量
dispatch的信号量是像其余的信号量同样的,若是你熟悉其余多线程系统中的信号量,那么这一节的东西再好理解不过了。
信号量是一个整形值而且具备一个初始计数值,而且支持两个操做:信号通知和等待。当一个信号量被信号通知,其计数会被增长。当一个线程在一个信号量上等待时,线程会被阻塞(若是有必要的话),直至计数器大于零,而后线程会减小这个计数。
咱们使用函数 dispatch_semaphore_create
来建立dispatch信号量,使用函数 dispatch_semaphore_signal
来信号通知,使用函数dispatch_semaphore_wait
来等待。这些函数的man页有两个很好的例子,展现了怎样使用信号量来同步任务和有限资源访问控制。
单次初始化
GCD还提供单次初始化支持,这个与pthread中的函数 pthread_once
很类似。GCD提供的方式的优势在于它使用block而非函数指针,这就容许更天然的代码方式:
这个特性的主要用途是惰性单例初始化或者其余的线程安全数据共享。典型的单例初始化技术看起来像这样(线程安全的):
这挺好的,可是代价比较昂贵;每次调用 +sharedWhatever
函数都会付出取锁的代价,即便这个锁只须要进行一次。确实有更风骚的方式来实现这个,使用相似双向锁或者是原子操做的东西,可是这样挺难弄并且容易出错。
使用GCD,咱们能够这样重写上面的方法,使用函数 dispatch_once
:
这个稍微比 @synchronized
方法简单些,而且GCD确保以更快的方式完成这些检测,它保证block中的代码在任何线程经过 dispatch_once
调用以前被执行,但它不会强制每次调用这个函数都让代码进行同步控制。实际上,若是你去看这个函数所在的头文件,你会发现目前它的实现实际上是一个宏,进行了内联的初始化测试,这意味着一般状况下,你不用付出函数调用的负载代价,而且会有更少的同步控制负载。
结论
这一章,咱们介绍了dispatch queue的挂起、恢复和目标重定,以及这些功能的一些用途。另外,咱们还介绍了如何使用dispatch 信号量和单次初始化功能。到此,我已经完成了GCD如何运做以及如何使用的介绍。
其主要思路是使用gcd建立串行队列,而后在此队列中前后执行两个任务:1.预加载一个viewController 2.将这个viewController推入
代码以下:
概述
我将分四步来带你们研究研究程序的并发计算。第一步是基本的串行程序,而后使用GCD把它并行计算化。若是你想顺着步骤来尝试这些程序的话,能够下载源码。注意,别运行imagegcd2.m,这是个反面教材。。
原始程序
咱们的程序只是简单地遍历~/Pictures而后生成缩略图。这个程序是个命令行程序,没有图形界面(尽管是使用Cocoa开发库的),主函数以下:
若是你要看到全部的副主函数的话,到文章顶部下载源代码吧。当前这个程序是imagegcd1.m。程序中重要的部分都在这里了。. Start
函数和 End
函数只是简单的计时函数(内部实现是使用的gettimeofday函数
)。ThumbnailDataForData函数使用Cocoa库来加载图片数据生成Image对象,而后将图片缩小到320×320大小,最后将其编码为JPEG格式。
简单而天真的并发
乍一看,咱们感受将这个程序并发计算化,很容易。循环中的每一个迭代器均可以放入GCD global queue中。咱们可使用dispatch queue来等待它们完成。为了保证每次迭代都会获得惟一的文件名数字,咱们使用OSAtomicIncrement32来原子操做级别的增长count 数:
这个就是imagegcd2.m,可是,注意,别运行这个程序,有很大的问题。
若是你无视个人警告仍是运行这个imagegcd2.m了,你如今颇有多是在重启了电脑后,又打开了个人页面。。若是你乖乖地没有运行这个程序的话,运行这个程序发生的状况就是(若是你有不少不少图片在~/Pictures中):电脑没反应,很久很久都不动,假死了。。
问题在哪
问题出在哪?就在于GCD的智能上。GCD将任务放到全局线程池中运行,这个线程池的大小根据系统负载来随时改变。例如,个人电脑有四核,因此若是我使用 GCD加载任务,GCD会为我每一个cpu核建立一个线程,也就是四个线程。若是电脑上其余任务须要进行的话,GCD会减小线程数来使其余任务得以占用 cpu资源来完成。
可是,GCD也能够增长活动线程数。它会在其余某个线程阻塞时增长活动线程数。假设如今有四个线程正在运行,忽然某个线程要作一个操做,好比,读文件,这 个线程就会等待磁盘响应,此时cpu核心会处于未充分利用的状态。这是GCD就会发现这个状态,而后建立另外一个线程来填补这个资源浪费空缺。
如今,想一想上面的程序发生了啥?主线程很是迅速地将任务不断放入global queue中。GCD以一个少许工做线程的状态开始,而后开始执行任务。这些任务执行了一些很轻量的工做后,就开始等待磁盘资源,慢得不像话的磁盘资源。
咱们别忘记磁盘资源的特性,除非你使用的是SSD或者牛逼的RAID,不然磁盘资源会在竞争的时候变得异常的慢。。
刚开始的四个任务很轻松地就同时访问到了磁盘资源,而后开始等待磁盘资源返回。这时GCD发现CPU开始空闲了,它继续增长工做线程。而后,这些线程执行更多的磁盘读取任务,而后GCD再建立更多的工资线程。。。
可能在某个时间文件读取任务有完成的了。如今,线程池中可不止有四个线程,相反,有成百上千个。。。GCD又会尝试将工做线程减小(太多使用CPU资源的 线程),可是减小线程是由条件的,GCD不能够将一个正在执行任务的线程杀掉,而且也不能将这样的任务暂停。它必须等待这个任务完成。全部这些状况都致使 GCD没法减小工做线程数。
而后全部这上百个线程开始一个个完成了他们的磁盘读取工做。它们开始竞争CPU资源,固然CPU在处理竞争上比磁盘先进多了。问题在于,这些线程读完文件 后开始编码这些图片,若是你有不少不少图片,那么你的内存将开始爆仓。。而后内存耗尽咋办?虚拟内存啊,虚拟内存是啥,磁盘资源啊。Oh shit!~
而后进入了一个恶性循环,磁盘资源竞争致使更多的线程被建立,这些线程致使更多的内存使用,而后内存爆仓致使虚拟内存交换,直至GCD建立了系统规定的线程数上限(多是512个),而这些线程又无法被杀掉或暂停。。。
这就是使用GCD时,要注意的。GCD能智能地根据CPU状况来调整工做线程数,可是它却没法监视其余类型的资源情况。若是你的任务牵涉大量IO或者其余会致使线程block的东西,你须要把握好这个问题。
修正
问题的根源来自于磁盘IO,而后致使恶性循环。解决了磁盘资源碰撞,就解决了这个问题。
GCD的custom queue使得这个问题易于解决。Custom queue是串行的。若是咱们建立一个custom queue而后将全部的文件读写任务放入这个队列,磁盘资源的同时访问数会大大下降,资源访问碰撞就避免了。
虾米是咱们修正后的代码,使用IO queue(也就是咱们建立的custom queue专门用来读写磁盘):
这个就是咱们的 imagegcd3.m
.
GCD使得咱们很容易就将任务的不一样部分放入相同的队列中去(简单地嵌套一下dispatch)。此次咱们的程序将会表现地很好。。。我是说多数状况。。。。
问题在于任务中的不一样部分不是同步的,致使了整个程序的不稳定。咱们的新程序的整个流程以下:
Main Thread IO Queue Concurrent Queue find paths ------> read -----------> process ... write <----------- process
图中的箭头是非阻塞的,而且会简单地将内存中的对象进行缓冲。
如今假设一个机器的磁盘足够快,快到比CPU处理任务(也就是图片处理)要快。其实不难想象:虽然CPU的动做很快,可是它的工做更繁重,解码、压缩、 编码。从磁盘读取的数据开始填满IO queue,数据会占用内存,极可能越占越多(若是你的~/Pictures中有不少不少图片的话)。
而后你就会内存爆仓,而后开始虚拟内存交换。。。又来了。。
这就会像第一次同样致使恶性循环。一旦任何东西致使工做线程阻塞,GCD就会建立更多的线程,这个线程执行的任务又会占用内存(从磁盘读取的数据),而后又开始交换内存。。
结果:这个程序要么就是运行地很顺畅,要么就是很低效。
注意若是磁盘速度比较慢的话,这个问题依旧会出现,由于缩略图会被缓冲在内存里,不过这个问题致使的低效比较不容易出现,由于缩略图占的内存少得多。
真正的修复
因为上一次咱们的尝试出现的问题在于没有同步不一样部分的操做,因此让我写出同步的代码。最简单的方法就是使用信号量来限制同时执行的任务数量。
那么,咱们须要限制为多少呢?
显然咱们须要根据CPU的核数来限制这个量,咱们又想马儿好又想马儿不吃草,咱们就设置为cpu核数的两倍吧。不过这里只是简单地这样处理,GCD的做用 之一就是让咱们不用关心操做系统的内部信息(好比cpu数),如今又来读取cpu核数,确实不太妙。也许咱们在实际应用中,能够根据其余需求来定义这个限 制量。
如今咱们的主循环代码就是这样了:
最终咱们写出了一个能平滑运行且又快速处理的程序。
基准测试
我测试了一些运行时间,对7913张图片:
程序处理时间 (秒)
imagegcd1.m |
984 |
imagegcd2.m |
没运行,这个仍是别运行了 |
imagegcd3.m |
300 |
imagegcd4.m |
279 |
注意,由于我比较懒。因此我在运行这些测试的时候,没有关闭电脑上的其余程序。。。严格的进行对照的话,实在是太蛋疼了。。
因此这个数值咱们只是参考一下。
比较有意思的是,3和4的执行情况差很少,大概是由于我电脑有15g可用内存吧。。。内存比较小的话,这个imagegcd3应该跑的很吃力,由于我发现它使用最多的时候,占用了10g内存。而4的话,没有占多少内存。
结论
GCD是个比较范特西的技术,能够办到不少事儿,可是它不能为你办全部的事儿。因此,对于进行IO操做而且可能会使用大量内存的任务,咱们必须仔细斟酌。固然,即便这样,GCD仍是为咱们提供了简单有效的方法来进行并发计算。