GCD 多线程

时间:2023-01-28 13:56:10
Grand Central Dispatch (GCD)是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并行任务。在Mac OS X 10.6雪豹中首次推出,也可在IOS 4及以上版本使用。

设计

GCD是一个替代诸如NSThread等技术的很高效和强大的技术。GCD完全可以处理诸如数据锁定和资源泄漏等复杂的异步编程问题。GCD的工作原理是让一个程序,根据可用的处理资源,安排他们在任何可用的处理器核心上平行排队执行特定的任务。这个任务可以是一个功能或者一个程序段。
GCD仍然在一个很低的水平使用线程,但是它不需要程序员关注太多的细节。GCD创建的队列是轻量级的,苹果声明一个GCD的工作单元需要由15个指令组成。也就是说创造一个传统的线程很容易的就会需要几百条指令。
GCD中的一个任务可被用于创造一个被放置于队列的工作项目或者事件源。若果一个任务被分配到一个事件源,那么一个由功能或者程序块组成的工作单元会被放置于一个适当的队列中。苹果公司认为GCD相比于普通的一个接一个的执行任务的方式更为有效率。

功能

这个调度框架声明了几种数据类型和函数来创建和操作他们:
一、调度队列
所有的调度队列都是先进先出队列,因此,队列中的任务的开始的顺序和添加到队列中的顺序相同。GCD自动的为我们提供了一些调度队列,我们也可以创建新的用于具体的目的。
下面列出几种可用的调度队列类型以及如何使用。
二、调度资源
它是一个监视某些类型事件的对象。当这些事件发生时,它自动将一个block放入一个调度队列的执行例程中。
三、调度组
允许将多任务分组来方便后来加入执行。任务能作为一个组中的一个成员被加到队列中,客户端能用这个组对象来等待直到这个组中的所有任务完成。
四、调度信号量
允许客户端并行发布一定数量的任务。

相关代码

在开始之前,需要理解是要提供给GCD队列的是代码块,用于在系统或者用户创建的的队列上调度运行。
1.声明一个队列
如下会返回一个用户创建的队列:
dispatch_queue_t myQueue = dispatch_queue_create("com.iphonedevblog.post", NULL);其中,第一个参数是标识队列的,第二个参数是用来定义队列的参数(目前不支持,因此传入NULL)。
2.执行一个队列
如下会异步执行传入的代码:
dispatch_async(myQueue, ^{ [self doSomething]; });其中,首先传入之前创建的队列,然后提供由队列运行的代码块。
3.声明并执行一个队列
如果不需要保留要运行的队列的引用,可以通过如下代码实现之前的功能:
dispatch_async(dispatch_queue_create ("com.iphonedevblog.post", NULL), ^{ [self doSomething]; });暂停一个队列
如果需要暂停一个队列,可以调用如下代码。暂停一个队列会阻止和该队列相关的所有代码运行。
dispatch_suspend(myQueue);恢复一个队列
如果暂停一个队列不要忘记恢复。暂停和恢复的操作和内存管理中的retain和release类似。调用dispatch_suspend会增加暂停计数,而dispatch_resume则会减少。队列只有在暂停计数变成零的情况下才开始运行。dispatch_resume(myQueue);
4.从队列中在主线程运行代码
有些操作无法在异步队列运行,因此必须在主线程(每个应用都有一个)上运行。UI绘图以及任何对NSNotificationCenter的调用必须在主线程长进行。在另一个队列中访问主线程并运行代码的示例如下:
dispatch_sync(dispatch_get_main_queue(), ^{ [self dismissLoginWindow]; });注意,dispatch_suspend (以及dispatch_resume)在主线程上不起作用。

实例

可以在John Siracusa的ArsTechnica博客文章Snow Leopard review中找到两个演示如何使用Grand Central Dispatch的例子。首先,一个处理文档的应用程序有一个名为analyzeDocument函数,会做一些统计文档的单词和段落的事情。通常情况下,这将是一个快速的过程,并在用户根本没注意到按下一个键和结果显示出来之间的延迟就已经在主线程中执行好了。
-(IBAction)analyzeDocument:(NSButton*)sender{
NSDictionary*stats=[myDoc analyze];
[myModel setDict:stats];
[myStatsViewsetNeedsDisplay:YES];
[stats release];
}
如果一个文档太大并且需要很长的时间去执行,那么主线程将会暂停等待这个函数完成。如果花了很长的时间,那么用户就会注意到这个延迟,应用程序甚至会没有响应。这个问题的解决方案如下:
-(IBAction)analyzeDocument:(NSButton*)sender{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^{
NSDictionary*stats=[myDoc analyze];
dispatch_async(dispatch_get_main_queue(),^{
[myModel setDict:stats];
[myStatsView setNeedsDisplay:YES];
[stats release];});
});
}
在这里,[myDoc analyze]的调用先被放置在了一个块中,然后进入一个全局并发队列里。在完成[myDoc analyze]的运行之后,一个新的块放置在主队列里(应用程序主线程在上面运行),更新了GUI(这是必要的,因为GUI只能由主线程更新)。通过这两个小的改动,开发人员就避免了能被用户看到的潜在的暂停并且使得应用程序更好的利用硬件资源。
第二个例子是一个并行的for循环:
for(i=0;i<count;i++){
results[i]=do_work(data,i);
}
total=summarize(results,count);
这段代码调用的do_work函数统计次数,将第i次的结果赋值给数组的第i个元素,然后在循环结束之后调用summarize函数。这些操作时顺序执行的,实际上并不需要这样。假设do_work函数不需要其他函数调用的结果,那么这些调用就可以同时运行了。这就是在GCD中的做法:
dispatch_apply(count,dispatch_get_global_queue(0,0),^(size_ti){
results[i]=do_work(data,i);
});
total=summarize(results,count);
这里,dispatch_apply运行传递来的块,在全局队列中放入每一次调用,并且给每次块调用一个从0到n-1的不同的数字。这样就允许操作系统通过当前的硬件和系统负载选择最佳的线程数来分配合适的工作。dispatch_apply知道所有的在给定队列的块运行完成才返回,这样就可以保证在调用summarize之前完成原来的循环里的所有事情。
程序员可以创建自己的任务队列,他们必须串行执行,但可在一个单独的线程中执行。一个新的队列可以像这样被创建:
dispatch_queue_t exampleQueue;
exampleQueue=dispatch_queue_create("com.example.unique.identifier",NULL);
//exampleQueue may be used here.
dispatch_release(exampleQueue);
必须小心,避免在队列中分配的块同步的放在同一队列的另一个块中,这是保证不会发生死锁情况。这样的代码可能会做下面的事情:
dispatch_queue_t exampleQueue=dispatch_queue_create("com.example.unique.identifier",NULL);
dispatch_sync(exampleQueue,^{
dispatch_sync(exampleQueue,^{
printf("I am now deadlocked...\n");
});
});
dispatch_release(exampleQueue);