Dispatch Queues
dispatch queues是执行任务的强大工具,允许你同步或异步地执行任意代码block。原先使用单独线程执行的所有任务都可以替换为使用dispatch queues。而dispatch queues最大的优点在于使用简单,而且更加高效。
dispatch queues任务的概念就是应用需要执行的一些工作,如计算、创建或修改数据结构、处理数据等等。我们使用函数或block对象来定义任务,并添加到dispatch queue。
dispatch queue是类似于对象的结构体,管理你提交给它的任务,而且都是先进先出的数据结构。因此queue中的任务总是以添加的顺序开始执行。Grand Central Disaptch提供了几种dispatch queues,不过你也自己创建。
类型 描述
串行 也称为private dispatch queue,每次只执行一个任务,按任务添加顺序执行。当前正在执行的任务在独立的线程中运行(不同任务的线程可能不同),dispatch queue管理了这些线程。通常串行queue主要用于对特定资源的同步访问。
你可以创建任意数量的串行queues,虽然每个queue本身每次只能执行一个任务,但是各个queue之间是并发执行的。
并发 也称为global dispatch queue,可以并发执行一个或多个任务,但是任务仍然是以添加到queue的顺序启动。每个任务运行于独立的线程中,dispatch queue管理所有线程。同时运行的任务数量随时都会变化,而且依赖于系统条件。
你不能创建并发dispatch queues。相反应用只能使用三个已经定义好的全局并发queues。
Main dispatch queue 全局可用的串行queue,在应用主线程中执行任务。这个queue与应用的 run loop 交叉执行。由于它运行在应用的主线程,main queue通常用于应用的关键同步点。
虽然你不需要创建main dispatch queue,但你必须确保应用适当地回收
应用使用dispatch queue,相比线程有很多优点,最直接的优点是简单,不用编写线程创建和管理的代码,让你集中精力编写实际工作的代码。另外系统管理线程更加高效,并且可以动态调控所有线程。
dispatch queue比线程具有更强的可预测性,例如两个线程访问共享资源,你可能无法控制哪个线程先后访问;但是把两个任务添加到串行queue,则可以确保两个任务对共享资源的访问顺序。同时基于queue的同步也比基于锁的线程同步机制更加高效。
应用有效地使用dispatch queue,要求尽可能地设计自包含、可以异步执行的任务。
dispatch queues的几个关键点:
dispatch queues相对其它dispatch queues并发地执行任务,串行化任务只能在同一个dispatch queue中实现。
系统决定了同时能够执行的任务数量,应用在100个不同的queues中启动100个任务,并不表示100个任务全部都在并发地执行(除非系统拥有100或更多个核)
系统在选择执行哪个任务时,会考虑queue的优先级。
queue中的任务必须在任何时候都准备好运行,注意这点和Operation对象不同。
private dispatch queue是引用计数的对象。你的代码中需要retain这些queue,另外dispatch source也可能添加到一个queue,从而增加retain的计数。因此你必须确保所有dispatch source都被取消,而且适当地调用release。
Queue相关的技术
除了dispatch queue,Grand Central Disaptch还提供几个相关的技术,使用queue来帮助你管理代码。
技术 描述
Dispatch group 用于监控一组block对象完成(你可以同步或异步地监控block)。Group提供了一个非常有用的同步机制,你的代码可以等待其它任务的完成
Dispatch semaphore 类似于传统的semaphore(信号量),但是更加高效。只有当调用线程由于信号量不可用,需要阻塞时,Dispatch semaphore才会去调用内核。如果信号量可用,就不会与内核进行交互。使用信号量可以实现对有限资源的访问控制
Dispatch source Dispatch source在特定类型的系统事件发生时,会产生通知。你可以使用dispatch source来监控各种事件,如:进程通知、信号、描述符事件、等等。当事件发生时,dispatch source异步地提交你的任务到指定的dispatch queue,来进行处理。使用Block实现任务
Block可以非常容易地定义“自包含”的工作单元,尽管看上去非常类似于函数指针,block实际上由底层数据结构来表示,由编译器负责创建和管理。编译器对你的代码(和所有相关的数据)进行打包,封装为可以存在于堆中的格式,并在你的应用各个地方传递。
Block最关键的优点能够使用own lexical scope之外的变量,在函数或方法内部定义一个block时,block可以直接读取父scope中的变量。block访问的变量全部被拷贝到block在堆中的数据结构,这样block就能在稍后自由地访问这些变量。当block被添加到dispatch queue中时,这些变量通常是只读格式的。不过同步执行的Block对象,可以使用那些定义为__block的变量,对这些变量的修改会影响到调用scope。
Block的简单用法:
int x = 123;
int y = 456;
// Block declaration and assignment
void (^aBlock)(int) = ^(int z) {
printf("%d %d %d\n", x, y, z);
};
// Execute the block
aBlock(789); // prints: 123 456 789
设计Block时需考虑以下关键指导方针:
对于使用dispatch queue的异步Block,可以在Block中安全地捕获和使用父函数或方法中的scalar变量。但是Block不应该去捕获大型结构体或其它基于指针的变量,这些变量由Block的调用上下文分配和删除。在你的Block被执行时,这些指针引用的内存可能已经不存在。当然,你自己显式地分配内存(或对象),然后让Block拥有这些内存的所有权,是安全可行的。
Dispatch queue对添加的Block会进行复制,在完成执行后自动释放。换句话说,你不需要在添加Block到Queue时显式地复制
尽管Queue执行小任务比原始线程更加高效,仍然存在创建Block和在Queue中执行的开销。如果Block做的事情太少,可能直接执行比dispatch到queue更加有效。使用性能工具来确认Block的工作是否太少
绝对不要针对底层线程缓存数据,然后期望在不同Block中能够访问这些数据。如果相同queue中的任务需要共享数据,应该使用dispatch queue的context指针来存储这些数据。
如果Block创建了大量Objective-C对象,考虑创建自己的autorelease pool,来处理这些对象的内存管理。虽然dispatch queue也有自己的autorelease pool,但不保证在什么时候会回收这些pool。
创建和管理Dispatch Queue
获得全局并发Dispatch Queue
并发dispatch queue可以同时并行地执行多个任务,不过并发queue仍然按先进先出的顺序来启动任务,并发queue会在之前任务完成之前就出列下一个任务并启动执行。并发queue同时执行的任务数量会根据应用和系统动态变化,各种因素包括:可用核数量、其它进程正在执行的工作数量、其它串行dispatch queue中优先任务的数量等。
系统给每个应用提供三个并发dispatch queue,所有应用全局共享,三个queue的区别是优先级。你不需要显式地创建这些queue,使用 dispatch_get_global_queue 函数来获取这三个queue:
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
除了默认优先级的并发queue,你还可以获得高和低优先级的两个,分别使用 DISPATCH_QUEUE_PRIORITY_HIGH 和 DISPATCH_QUEUE_PRIORITY_LOW 常量来调用上面函数。
虽然dispatch queue是引用计数的对象,但你不需要retain和release全局并发queue。因为这些queue对应用是全局的,retain和release调用会被忽略。
你也不需要存储这三个queue的引用,每次都直接调用 dispatch_get_global_queue 获得queue就行了。
创建串行Dispatch Queue
应用的任务需要按特定顺序执行时,就需要使用串行Dispatch Queue,串行queue每次只能执行一个任务。你可以使用串行queue来替代锁,保护共享资源或可变的数据结构。和锁不一样的是,串行queue确保任务按可预测的顺序执行。而且只要你异步地提交任务到串行queue,就永远不会产生死锁。
你必须显式地创建和管理所有你使用的串行queue,应用可以创建任意数量的串行queue,但不要为了同时执行更多任务而创建更多的串行queue。如果你需要并发地执行大量任务,应该把任务提交到全局并发Queue。
创建串行queue时,你需要明确自己的目的,如保护共享资源,或同步应用的某些关键行为。
dispatch_queue_create 函数创建串行queue,两个参数分别是queue名和一组queue属性。调试器和性能工具会显示queue的名字,便于你跟踪任务的执行。
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);
运行时获得公共Queue
Grand Central Disaptch提供函数,让应用访问几个公共dispatch queue:
使用 dispatch_get_current_queue 函数作为调试用途,或者测试当前queue的标识。在block对象中调用这个函数会返回block提交到的queue(这个时候queue应该正在执行中)。在block对象之外调用这个函数会返回应用的默认并发queue。
使用 dispatch_get_main_queue 函数获得应用主线程关联的串行dispatch queue。Cocoa 应用、调用了 dispatch_main 函数或配置了run loop(CFRunLoopRef 类型 或一个 NSRunLoop 对象)的应用,会自动创建这个queue。
使用 dispatch_get_global_queue 来获得共享的并发queue
Dispatch Queue的内存管理
Dispatch Queue和其它dispatch对象都是引用计数的数据类型。当你创建一个串行dispatch queue时,初始引用计数为1,你可以使用 dispatch_retain 和 dispatch_release 函数来增加和减少引用计数。当引用计数到达0时,系统会异步地销毁这个queue。
对dispatch对象(如queue)retain和release是很重要的,确保它们被使用时能够保留在内存中。和内存托管的Cocoa对象一样,通用的规则是如果你使用一个传递给你代码中的queue,你应该在使用前retain,使用完之后release。
你不需要retain或release全局dispatch queue,包括全局并发 dispatch queue和main dispatch queue。
即使你实现的是自动垃圾收集的应用,也需要retain和release你的dispatch queue和其它dispatch对象。Grand Central Disaptch不支持垃圾收集模型来回收内存。
在Queue中存储自定义上下文信息
所有dispatch对象(包括dispatch queue)都允许你关联custom context data。使用 dispatch_set_context 和 dispatch_get_context 函数来设置和获取对象的上下文数据。系统不会使用你的上下文数据,所以需要你自己在适当的时候分配和销毁这些数据。
对于Queue,你可以使用上下文数据来存储一个指针,指向Objective-C对象或其它数据结构,协助标识这个queue或代码的其它用途。你可以使用queue的finalizer函数来销毁(或解除关联)上下文数据。
为Queue提供一个清理函数
在创建串行dispatch queue之后,可以附加一个finalizer函数,在queue被销毁之前执行自定义的清理操作。使用 dispatch_set_finalizer_f 函数为queue指定一个清理函数,当queue的引用计数到达0时,就会执行该清理函数。你可以使用清理函数来解除queue关联的上下文数据,而且只有上下文指针不为NULL时才会调用这个清理函数。
下面例子演示了自定义finalizer函数的使用,你需要自己提供 myInitializeDataContextFunction 和 myCleanUpDataContextFunction 函数,用于初始化和清理上下文数据。
void myFinalizerFunction(void *context)
{
MyDataContext* theData = (MyDataContext*)context;
// Clean up the contents of the structure
myCleanUpDataContextFunction(theData);
// Now release the structure itself.
free(theData);
}
dispatch_queue_t createMyQueue()
{
MyDataContext* data = (MyDataContext*) malloc(sizeof(MyDataContext));
myInitializeDataContextFunction(data);
// Create the queue and set the context data.
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL);
if (serialQueue)
{
dispatch_set_context(serialQueue, data);
dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);
}
return serialQueue;
}
添加任务到Queue
要执行一个任务,你需要将它dispatch到一个适当的dispatch queue,你可以同步或异步地dispatch一个任务,也可以单个或按组来dispatch。一旦进入到queue,queue会负责尽快地执行你的任务。
添加单个任务到Queue
你可以异步或同步地添加一个任务到Queue,尽可能地使用 dispatch_async 或 dispatch_async_f 函数异步地dispatch任务。因为添加任务到Queue中时,无法确定这些代码什么时候能够执行。因此异步地添加block或函数,可以让你立即调度这些代码的执行,然后调用线程可以继续去做其它事情。
特别是应用主线程一定要异步地dispatch任务,这样才能及时地响应用户事件。
少数时候你可能希望同步地dispatch任务,以避免竞争条件或其它同步错误。使用 dispatch_sync 和 dispatch_sync_f 函数同步地添加任务到Queue,这两个函数会阻塞,直到相应任务完成执行。
绝对不要在任务中调用 dispatch_sync 或 dispatch_sync_f 函数,并同步dispatch新任务到当前正在执行的queue。对于串行queue这一点特别重要,因为这样做肯定会导致死锁;而并发queue也应该避免这样做。
dispatch_queue_t myCustomQueue;
myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL);
dispatch_async(myCustomQueue, ^{
printf("Do some work here.\n");
});
printf("The first block may or may not have run.\n");
dispatch_sync(myCustomQueue, ^{
printf("Do some more work here.\n");
});
printf("Both blocks have completed.\n");
任务完成时执行Completion Block
dispatch到queue中的任务,通常与创建任务的代码独立运行。在任务完成时,应用可能希望得到通知并使用任务完成的结果数据。在传统的异步编程模型中,你可能会使用回调机制,不过dispatch queue允许你使用Completion Block。
Completion Block是你dispatch到queue的另一段代码,在原始任务完成时自动执行。调用代码在启动任务时通过参数提供Completion Block。任务代码只需要在完成工作时提交指定的Block或函数到指定的queue。
下面代码使用block实现了平均数,最后两个参数允许调用方指定一个queue和报告结果的block。在平均数函数完成计算后,会传递结果到指定的block,并dispatch到指定的queue。为了防止queue被过早地释放,必须首先retain这个queue,然后在dispatch这个Completion Block之后,再release这个queue。
void average_async(int *data, size_t len,
dispatch_queue_t queue, void (^block)(int))
{
// Retain the queue provided by the user to make
// sure it does not disappear before the completion
// block can be called.
dispatch_retain(queue);
// Do the work on the default concurrent queue and then
// call the user-provided block with the results.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
int avg = average(data, len);
dispatch_async(queue, ^{ block(avg);});
// Release the user-provided queue when done
dispatch_release(queue);
});
}
并发地执行Loop Iteration
如果你使用循环执行固定次数的迭代,并发dispatch queue可能会提高性能。例如下面for循环:
for (i = 0; i < count; i++) {
printf("%u\n",i);
}
如果每次迭代执行的任务与其它迭代独立无关,而且循环迭代执行顺序也无关紧要的话,你可以调用 dispatch_apply 或 dispatch_apply_f 函数来替换循环。这两个函数为每次循环迭代将指定的block或函数提交到queue。当dispatch到并发queue时,就有可能同时执行多个循环迭代。
调用 dispatch_apply 或 dispatch_apply_f 时你可以指定串行或并发queue。并发queue允许同时执行多个循环迭代,而串行queue就没太大必要使用了。
和普通for循环一样,dispatch_apply 和 dispatch_apply_f 函数也是在所有迭代完成之后才会返回。因此在queue上下文执行的代码中再次调用这两个函数时,必须非常小心。如果你传递的参数是串行queue,而且正是执行当前代码的Queue,就会产生死锁。
另外这两个函数还会阻塞当前线程,因此在主线程中调用这两个函数同样必须小心,可能会阻止事件处理循环并无法响应用户事件。所以如果循环代码需要一定的时间执行,你可以考虑在另一个线程中调用这两个函数。
下面代码使用 dispatch_apply 替换了for循环,你传递的block必须包含一个参数,用来标识当前循环迭代。第一次迭代这个参数值为0,第二次时为1,最后一次值为count - 1。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(count, queue, ^(size_t i) {
printf("%u\n",i);
});
循环迭代执行的工作量需要仔细平衡,太多的话会降低响应性;太少则会影响整体性能,因为调度的开销大于实际执行代码。
在主线程中执行任务
Grand Central Disaptch提供一个特殊dispatch queue,可以在应用的主线程中执行任务。应用主线程设置了run loop(由CFRunLoopRef 类型或 NSRunLoop 对象管理),就会自动创建这个queue,并且自动drain。非Cocoa应用如果不显式地设置run loop,就必须显式地调用dispatch_main 函数来显式地drain这个dispatch queue。否则虽然你可以添加任务到queue,但任务永远不会被执行。
调用 dispatch_get_main_queue 函数获得应用主线程的dispatch queue。添加到这个queue的任务由主线程串行化执行,因此你可以在应用的某些地方使用这个queue作为同步点。
任务中使用Objective-C对象
Grand Central Disaptch支持Cocoa内存管理机制,因此可以在提交到queue的block中自由地使用Objective-C对象。每个dispatch queue维护自己的autorelease pool确保释放autorelease对象,但是queue不保证这些对象实际释放的时间。在自动垃圾收集的应用中,Grand Central Disaptch会在垃圾收集系统中注册自己创建的每个线程。
如果应用消耗大量内存,并且创建大量autorelease对象,你需要创建自己的autorelease pool,用来及时地释放不再使用的对象。
挂起和继续queue
我们可以暂停一个queue以阻止它执行block对象,使用 dispatch_suspend 函数挂起一个dispatch queue;使用 dispatch_resume 函数继续dispatch queue。调用 dispatch_suspend 会增加queue的引用计数,调用 dispatch_resume 则减少queue的引用计数。当引用计数大于0时,queue就保持挂起状态。因此你必须对应地调用suspend和resume函数。
挂起和继续是异步的,而且只在执行block之间生效。挂起一个queue不会导致正在执行的block停止。
使用Dispatch Semaphore控制有限资源的使用
如果提交到dispatch queue中的任务需要访问某些有限资源,可以使用dispatch semaphore来控制同时访问这个资源的任务数量。dispatch semaphore和普通的信号量类似,唯一的区别是当资源可用时,需要更少的时间来获得dispatch semaphore。
使用dispatch semaphore的过程如下:
使用 dispatch_semaphore_create 函数创建semaphore,指定正数值表示资源的可用数量。
在每个任务中,调用 dispatch_semaphore_wait 来等待Semaphore
当上面调用返回时,获得资源并开始工作
使用完资源后,调用 dispatch_semaphore_signal 函数释放和signal这个semaphore
// Create the semaphore, specifying the initial pool size
dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2);
// Wait for a free file descriptor
dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
fd = open("/etc/services", O_RDONLY);
// Release the file descriptor when done
close(fd);
dispatch_semaphore_signal(fd_sema);
等待queue中的一组任务
Dispatch group用来阻塞一个线程,直到一个或多个任务完成执行。有时候你必须等待任务完成的结果,然后才能继续后面的处理。dispatch group也可以替代线程join。
基本的流程是设置一个组,dispatch任务到queue,然后等待结果。你需要使用 dispatch_group_async 函数,会关联任务到相关的组和queue。使用 dispatch_group_wait 等待一组任务完成。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
// Add a task to the group
dispatch_group_async(group, queue, ^{
// Some asynchronous work
});
// Do some other work while the tasks execute.
// When you cannot make any more forward progress,
// wait on the group to block the current thread.
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// Release the group when it is no longer needed.
dispatch_release(group);
Dispatch Queue和线程安全性
使用Dispatch Queue实现应用并发时,也需要注意线程安全性:
Dispatch queue本身是线程安全的。换句话说,你可以在应用的任意线程中提交任务到dispatch queue,不需要使用锁或其它同步机制。
不要在执行任务代码中调用 dispatch_sync 函数调度相同的queue,这样做会死锁这个queue。如果你需要dispatch到当前queue,需要使用 dispatch_async 函数异步调度
避免在提交到dispatch queue的任务中获得锁,虽然在任务中使用锁是安全的,但在请求锁时,如果锁不可用,可能会完全阻塞串行queue。类似的,并发queue等待锁也可能阻止其它任务的执行。如果代码需要同步,就使用串行dispatch queue。
虽然可以获得运行任务的底层线程的信息,最好不要这样做。