本文源自https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/ConcurrencyProgrammingGuide.pdf
Dispatch Queues
GCD(Grand Central Dispatch)dispatch queues是一个执行任务的强大工具。对于调用者,dispatch queues可以让你同步或者异步地执行任意的代码块。你可以使用dispatch
queues来执行你之前用分离的线程执行的几乎所有的任务。dispatch queues相对于线程代码的优势是它们在执行任务时更加方便易用并且更加高效率。
本章将介绍dispatch queues,以及如何使用它们来执行你应用中常见的任务。如果你希望使用dispatch
queues取代已经存在的线程代码,你可以在Migrating Away from Threads部分找到更多的信息。
关于dispatch queues
dispatch queues是一种简单的方式来异步和并发地执行你应用中的任务。一个任务就是你的应用需要执行的一些简单的工作。比如,你可以定义一个任务为:执行一些计算创建或者修改数据结构,处理从文件读取的数据,或者其他的一些事情。你使用相应的代码或是函数或者一个block对象定义任务然后将它们添加到dispatch
queues。
一个dispatch queues是一个类似于对象的结构来管理你提交给它的任务。所有的dispatch queues都是先进先出的数据结构。因此,你添加到queue中的任务总是以添加时的同样的顺序执行。GCD自动地为你提供一些dispatch
queues,但是你可以因为特殊的目的创建一些其他的dispatch queues。
当为一个应用添加并发时,dispatch queues比线程提供了一些优势。最直接的优势是工作队列编程模型的简单性。使用线程,你必须编写你希望执行的任务的代码和创建和管理线程的代码。dispatch queues让你集中精力在你实际想要执行的任务上而不是担心线程的创建和管理。系统为你处理所有的线程创建和管理工作。优势是系统可以比你单个的应用程序更加高效的管理线程。系统可以通过可用资源和当前系统情况动态地均衡线程数量。其次,系统通常比你自己创建线程的方法更加迅速的开始运行你的任务。
尽管你可能认为为了dispatch queues重写代码比较困难,通常为dispatch queues写代码比为线程写代码要简单。写代码的关键是设计你的任务成为自包含的能够异步运行的。(这实际上对于dispatch queues和线程都是这样的)然而,dispatch queues拥有的优势是可预见的。如果你有两个任务需要访问一个共享的资源但是运行在不同的线程上,任何一个线程都不能首先修改这个资源,你需要使用一个锁来确保两个任务不同时修改这个资源。使用dispatch
queues,你可以添加两个任务到一个串行的dispatch queue来确保一个时间内只有一个任务能够修改资源。这种基于队列的同步方式比锁更加有效率,因为在竞争和非竞争的情况下,锁通常需要昂贵的内核中断,而dispatch queues主要运行在你的应用的进程空间只有在绝对必要时才调用内核。
尽管你可能指出这两个任务运行在一个串行的队列中不能并发的执行,你需要记住两个线程在同一个时间有一个锁,线程提供的任何并发都会丢失或者严重的减少。更重要的是,线程模型需要创建两个线程,占有内核和用户空间的内存。dispatch queues没有为它们的线程付出同样的内存损失,它们使用的线程保持忙碌而且不会阻塞。
一些dispatch queues其他的关键点需要记住的如下:
1. dispatch queues并发的执行它们的任务,独立于其他的dispatch queues。串行的任务只有在同一个dispatch
queue中受限制。
2. 系统决定任何一个时间内执行的任务的总数。因此,一个应用有100个任务在100个不同的queues中可能不能并发的执行这些任务。(除非有100或者更多的有用的内核)
3. 系统在选择执行哪个新任务时,会把queue的优先级作为参考。
4. 队列中的任务必须在添加到队列时就是准备好执行的。
5. 私有的dispatch queues是引用计数对象。除了在你自己的代码中retain队列,注意dispatch resources可以添加一个queue并且也可以增加它的retain数量。因此,你必须确保所有的dispatch sources被退出并且所有的retain调用都有一个正确的release调用。
队列相关的技术
除了dispatch queues,GCD提供了一些使用queue的技术来帮助管理你的代码。
使用Blocks来执行任务
Blocks对象是一种你可以在C,C++和Objective-C中使用的基于C语言的特征。Blocks可以很容易地定义一个自包含的工作单元。尽管它们看起来像函数指针,一个block实际代表了一个基本的数据结构就像一个编译器为你创建和管理的对象。编译器打包你提供的代码,以一种形式封装它们,它们可以存活在堆中,可以在你的应用中到处被传递。
blocks的一种关键优势是它们有能力使用它们自己语法范围外的变量。当你在函数或方法内部定义了一个block,block在一些情况下作为传统的代码块。比如,一个block可以读取parent范围内的变量的值。block访问的变量被复制到堆上的block的数据结构上,所有block可以稍后访问这些变量。当blocks被添加到一个dispatch queue上,这些值这个值必须变成只读的格式。然而,同步执行的block可以使用包含__block关键字前缀的变量返回数据给parent调用的作用域中。
你声明内联blocks代码的语法和使用函数指针的语法类似。函数指针和block的主要的不同之处是block的名字是以一个插入符号(^)而函数指针是星号(*)。和函数指针一样,你可以传递参数给block,从block接收返回值。下面为你展示了在你的代码中如何声明和同步执行blocks。
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
下面是一个在设计你的blocks时你需要考虑的关键指导方针的总结:
1. 你希望使用dispatch queue异步地执行的blocks,它可以完全地从parent函数或者方法中获取标量值并且在block中使用。然而,你不应该尝试去获取大的结构体或者其他被上下文分配和删除的基于指针的变量。你block执行时,指针指向的内存引用可以已经消息了。当然,你可以安全的分配内存并且显示的处理block的内存。
2. dispatch queues复制添加到它的blocks,当blocks完成执行时,dispatch queue就释放了它们。换句话说,你不需要在在添加blocks到队列之前显示的复制它们。
3. 尽管queues比原生的线程执行小任务时更加有效率,仍然有创建blocks和在queue上执行它们的开销。如果一个block执行太少的任务,可能内联的执行它比安排到队列上执行更加廉价。评价一个block是否做的工作太少的方式是使用性能工具收集每种方式的参数然后比较它们。
4. 不要缓存基础线程相关的数据并期待这些数据可以从一个不同block访问。如果同一个queue中的任务需要共享数据,使用dispatch queue的context指针来存储这些数据。
5. 如果你的block创建很多ObjectiveC objects,你可能希望封装你block部分代码到@autorelease block来为这些对象处理内存管理的问题。尽管GCD dispatch queue有它自己的autorelease pools,它们不能保证这些pool什么时候被drain。如果你的程序是内存受限制的,创建你自己的autorelease
pool允许你为这些autorelease对象以更加规则的间隔释放内存。
创建和管理Dispatch Queues
你添加任务到queue之前,你必须决定使用什么类型的queue并且你打算怎样使用它。Dispatch queue可以串行或者并行的执行任务。另外,如果你对queue有一些特殊的要求,你可以通过配置queue的属性。下面的部分为你展示如何创建和配置dispatch queue。
获取全局并发Dispatch queue
一个并发的Dispatch queue在你有多个任何并行执行时很有用。一个并发的Dispatch queue仍然是先进先出的队列的;然而,一个并发的queue可能在任何前面任务完成前出列额外的任务。一个并发的Dispatch
queue在任何一个时间实际执行的任务数是一个随着你应用程序的变化而变化的变量。很多因素会影响同时执行的任务数,包括可用的cpu核心数,被其他进程处理的工作总量,在其他串行Dispatch queue中的优先级高的任务数量。
系统为每个应用提供4个并发的Dispatch queue。这些queues对于应用来说都是全局的,它们的区别仅仅在于优先级不同。因为它们是全局的,你不需要显示的创建它们。相反,你通过使用dispatch_get_global_queue函数来获取其中的一个,就像下面显示的。
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
除了获取默认的并发队列,你也可以获取高优先级和低优先级的队列通过传入DISPATCH_QUEUE_PRIORITY_HIGH和DISPATCH_QUEUE_PRIORITY_LOW常量。或者通过传入DISPATCH_QUEUE_PRIORITY_BACKGROUND常量获取一个background queue。就像你预计的那样,高优先级的并发队列中的任务在低优先级队列中的任务之前执行。类似的,默认队列中的任务比在低优先级中的任务之前执行。
注意:dispatch_get_global_queue函数的第二个参数是为了未来扩展而保留的。现在,你必须总是传递0.
尽管dispatch queue是引用计数的对象,你不需要retain和release全局并发队列。因为它们对于应用是全局的,对于这些队列的retain和release会被直接忽略。因此,你不需要存储这些队列的引用。当你需要其中一个的引用时你仅仅需要调用dispatch_get_global_queue。
创建串行dispatch queue
串行队列在你希望你的任务以一定的顺序执行时很有用。一个串行的队列一次只执行一个任务并且总是从队列的头上取任务。你可能使用一个串行队列取代锁来保护一个共享的资源或者可变的数据结构。不象锁,一个串行的队列保证任务以一个可预见的顺序执行。你异步的提交你的任务到一个串行的队列中,这个队列永远也不会死锁。
不象并发队列是已经为你创建了,你必须显示的创建和管理任何你需要使用的串行队列。你可以创建任意数量的串行队列,但是需要避免创建大量单独的串行队列来尽可以多的执行你想同时执行的任务。如果你希望并发的执行大量的任务,把它们提交到一个全局的并发队列中。当创建一个串行队列时,尝试定义每一个队列的目的,比如保护资源或者同步你应用的一些关键行为。
Listing3-2展示了创建一个自定义的串行队列需要的步骤。dispatch_queue_create函数需要2个参数:queue的名字和queue的属性的集合。调试器和性能工具显示这个queue名字来帮助你跟踪你的任务是如何被执行的。queue的属性是为未来保留的必须是NULL
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);
除了你创建的任何自定义的队列,系统自动创建了一个串行队列并且绑定它到你的主线程。
在runtime时获取常见的Queues
GCD提供函数让你访问你的应用的一些普通的dispatch queue:
1. 出于调试目的或者测试当前queue的标识符时使用dispatch_get_current_queue函数。在一个block对象内部调用这个函数返回这个block提交的队列。在一个block外面调用这个函数返回你应用程序默认的并发队列。
2. 使用dispatch_get_main_queue函数来获取你应用的主线程的串行dispatch queue。这个queue是自动的为Cocoa应用创建的,应用也可以通过调用dispatch_main函数或者在主线程上配置run loop。
3. 使用dispatch_get_global_queue函数获取任何共享的并发队列。
Dispatch queues的内存管理
Dispatch queues和dispatch对象是引用计数的数据类型。当你创建一个串行dispatch queue时,它的引用计数被初始化为1。你可以使用dispatch_retain和dispatch_release函数根据需要来增加和减少应用计数。当queue的引用计数达到0时,系统异步的释放这个queue。
retain和release dispatch对象是很重要的,比如queues,确保它们被使用时仍然在内存中。对于Cocoa对象的内存管理,基本的规则就是如果你打算使用一个传入你代码的queue,你必须在使用这个queue之前retain,不需要它以后release。这个基本的方式确保你正在使用它的时候这个queue就在内存中。
注意:你不需要retain或者release全局的dispatch queues。包括并发dispatch queue或者main dispatch queue。任何企图retain和release这些queues都会被忽略。
就算你实现了一个垃圾收集的程序,你仍然必须retain和release你的dispatch queues和其他的dispatch对象。GCD不支持垃圾收集模型来回收内存。
用Queue存储用户上下文信息
所有的dispatch对象允许你关联一个用户上下文数据。在一个给定的对象上设置和获取这个数据,你使用dispatch_set_context和dispatch_get_context函数。系统不使用你的用户数据,在正确的时间分配和释放用户数据这完全取决于你。
对于Queues,你可以使用上下文数据来存储一个指向一个ObjectiveC对象的指针或者其他的数据结构帮助你区分queue和它的用途。你可以使用queue的finalizer函数在queue被释放之前释放queue上你的上下文数据。
为Queue提供一个清理函数
你创建了串行dispatch queue以后,你可以附加一个finalizer函数在queue被释放时执行任何用户清理工作。Dispatch queues是引用计数的对象你可以使用dispatch_set_finalizer_f函数来指定一个函数,当queue的引用计数达到0时被执行。你使用这个函数来清理queue关联的上下文数据并且这个函数只有在上下文指针不是NULL时才被调用。
下面显示一个用户finalizer函数和一个函数创建一个queue并且安装finalizer。
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;
}
{
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它们。一旦在队列中,队列就会尽快为你执行你的任务,鉴于其约束和现有的任务已经在队列。本部分为你一些关于dispatch任务到queue上的技术以及描述它们每一个的优点。
添加一个单独的任务到队列上
有两种方法来添加一个任务到队列上:异步或者同步。尽可能优先通过dispatch_async和dispatch_async_f函数使用异步执行而不是同步执行。当你添加一个block对象或者函数到一个queue,没有办法知道代码什么时候会被执行。其结果就是,异步的添加blocks或者函数让你安排代码的执行然后在调用线程中继续做其他的事情。你从主线程上安排任务时显得尤为重要--响应于一些用户事件。
尽管你应该尽可能异步的添加任务,仍然有一些情况你需要同步的添加任务来阻止竞争条件或者其他同步错误。在这些情况下,你可以使用dispatch_sync和dispatch_sync_f函数来添加任务到queue上。这些函数阻塞当前线程的执行直到指定的仍无完成执行。
重要:你永远不能从一个正在同一个队列(这个队列是你正打算传递给函数的)中执行的任务调用dispatch_sync或者dispatch_sync_f函数。这对于串行队列尤为重要,因为会产生死锁,同样需要在并发队列中避免。
下面的例子显示了如何使用基于block的变量来同步和异步的分发任务。
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”);
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”);
当任务完成时执行一个结束block
就其性质而言,分发到队列的任务独立于创建它们的代码运行。然而,当任务完成后,你的应用可能仍然希望在任务完成时被通知来使用这个结果。传统的异步编程,你可能使用一个回调机制,但是在dispatch queues中你可以使用一个结束block。
一个结束block仅仅就是另外一段代码,你放到原始任务的最后,你希望分发到队列中执行。调用代码典型的方式是在开始任务时以一个参数的方式提供这个结束block。这个任务要做的就是当它完成它的工作后提交一个指定的block或者函数到这个指定的queue中。
下面展示了一个使用block实现的averaging函数。averaging函数的最后两个参数允许调用者在报告结果时指定一个queue和block来使用。averaging函数计算它的值以后,传递这个结果给指定的block然后分发给queue。为了阻止这个queue过早的被释放,它在初始化时retain那个queue并且在结束block被分发后就释放了它。
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);
});
}
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);
});
}
并发的执行循环迭代
有一个地方并发dispatch queue可能会提高性能就是你有一个执行固定数量的迭代的循环。比如,假如你有一个for循环在每次迭代中都做相同的事情:
for (i = 0; i <
count; i++) {
printf("%u\n",i);
}
printf("%u\n",i);
}
如果每次迭代中完成的工作是在所有其他迭代完成的工作截然不同,并且每次循环执行完成的顺序不重要的,你可以使用dispatch_apply或者dispatch_apply_f函数来取代循环。这些函数把每一次的循环迭代以指定的block或者函数一次性提交到queue中。当分发到一个并发队列后,同时可能执行多个循环迭代。
调用dispatch_apply或者dispatch_apply_f函数你可以指定串行的或者并发的队列。传入一个并发queue允许你同时执行多个循环迭代并且这时使用这些函数最常见的方式。尽管可以使用一个串行的队列是允许的,使用这个一个queue没有实际的性能优势。
重要:就像一个常规的循环,dispatch_apply或者dispatch_apply_f函数直到所有的循环执行完成后才返回。在代码中调用它们时你必须注意。如果你把这个queue(这个queue是一个串行的queue并且是执行当前代码的queue)当作一个参数传递给这些函数,调用这些函数会导致queue死锁。
因为它们实际上阻塞当前线程,在主线程中调用时必须注意,它们可能会阻止你的事件处理循环及时的响应事件。如果你的循环代码需要一定的时间,你可能希望从一个不同的线程调用这些函数。
listing3-5展示了使用dispatch_apply如何取代之前的for循环的语法。你传递给dispatch_apply函数的block必须包含一个标识当前循环遍历的简单的参数。当block被执行了,这个参数的值在第一次遍历时是0,第二次是1,依此类推。最后一次遍历的值是count-1,count是遍历的总数。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(count, queue, ^(size_t i) {
printf("%u\n",i);
});
dispatch_apply(count, queue, ^(size_t i) {
printf("%u\n",i);
});
你必须确保你的任务代码在每次遍历做一些合适工作量的工作。任何你dispatch到queue的block或函数,安排这些代码的执行会有一些开销。如果你循环每次遍历都仅仅做很少一点工作,调度代码的开销可能比你dispatch它到queue上带来的性能优势要大。如果在测试的过程中你发现这时真的,你可以使用跨步(i+5)来增加你每次循环遍历的工作量。通过跨步(striding),你把个多遍历汇集到一个单一的block中来成比例的减少遍历次数。比如,如果你执行100次遍历,但是你决定使用跨步为4,你在每个block中执行4个遍历,那么你的遍历数就是25.
在主线程上执行任务
GCD提供了一个专门的dispatch queue你可以用来在你应用程序的主线程执行任务。这个queue为所有的应用自动提供并且被那些设置了run loop的应用自动drained。如果你不创建一个Cocoa应用,不希望显示的设置一个run
loop,你必须调用dispatch_main函数来显示的drain这个主dispatch queue。你仍然可以添加任务到这个queue,但是你不能调用这个函数这些任务永远也不会被执行
你可以通过调用dispatch_get_main_queue函数来为你应用的主线程获得这个dispatch queue。在主线程上添加的任务被串行的执行。因此,你可以使用这个队列作为同步点在你的应用程序的其他部分正在做的工作。
在你的任务中使用ObjectiveC对象
GCD为Cocoa提供了内置的内存管理技术的支持所以你可以在你提交到dispatch queues的blocks中自由的使用ObjectiveC对象。每个dispatch queue维持它自己的autorelease pool来确保那些autoreleased对象在合适的点被释放;queues不能保证它们什么时候真正的释放这些对象。
如果你的应用是内存受限的并且你的block创建了大量的autoreleased对象,创建你自己的autorelease pool是唯一的方式来确保你的对象及时的释放。如果你的blocks创建了大量的对象,你可能希望创建更多的autorelease pool或者以经常地drain你的pool。
暂停和恢复Queues
你通过暂停队列来暂时的阻止一个队列执行block对象。你通过使用dispatch_suspend函数来暂停一个dispatch queue,使用dispatch_resume函数来恢复它。调用dispatch_suspend增长queue的暂停引用计数,调用dispatch_resume降低这个引用计数。当这个引用计数大于0时,queue仍然是暂停的。因此你必须用恢复调用来平衡所有的暂停调用来恢复正在处理的blocks。
重要:暂时和恢复调用是异步的并且只会在执行的blocks上有作用。暂停一个queue不会造成一个已经执行的block停止。
使用Dispatch Semaphores来调节受限资源的使用
如果你提交到一个dispatch queues的任务访问一些受限资源,你可能希望使用一个dispatch semaphore来调整同时访问资源的任务数。一个dispatch semaphore就像有一个异常的规则的信号量。当资源是可用时,它获取一个dispatch semaphore比获取传统的系统信号量使用更少的时间。这是因为GCD在这种特殊的情况不会调用到内核。它仅仅在资源不可用时才调用内核,系统需要暂停你的线程直到信号量被signaled。
使用一个dispatch semaphore的语法如下:
1. 当你创建semaphore(使用dispatch_semaphore_create函数),你可以指定一个正值来指定可用的资源数。
2. 在每一次任务中,调用dispatch_semaphore_wait来等待信号量
3. 当wait调用返回时,获取这个资源然后做你的工作。
4. 当你使用完这个资源后,释放它并且通过调用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);
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);
当你创建semaphore时,你指定可用的资源数。此值将成为该信号量的初始计数变量。每次你在semaphore上等待,dispatch_semaphore_wait函数就把这个值减1,如果这个值是负数了,函数告诉内核来阻塞你的线程。另一个方面,dispatch_semaphore_signal函数把这个值加1表示资源被释放了。如果有任务阻塞并且等待资源,他们中的一个会变成非阻塞然后被允许执行。
等待一组Queued任务。
Dispatch groups是一种阻塞一个线程直到一个或多个任务完成执行的方式。你可以使用这个行为在你需要等待所有指定的任务完成后才开始执行你的任务。比如,在分发一些任务来计算之后,你可能使用一个group来等待这些任务然后使在它们完成后处理这些结果。另外一种方式来使用dispatch groups是thread join的替换。你可以添加响应的任务到一个dispatch group然后等待整个group而不是在开始一些子线程然后joining每一个线程。
Listing3-6展示基本的过程来设置一个group,分发任务给它,等待它的结果:
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_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 queues和线程安全
在dispatch queues的上下文来谈论线程安全看起来很奇怪,但是线程安全仍然是相关的话题。在你应用中,实现并发的任何时候,有一些事情你必须要知道:
1. dispatch queues它们本身是线程安全的。换句话说,你可以从系统的任何线程上提交任务到一个dispatch queue,不需要首先使用一个锁或者同步访问queue。
2. 不要从一个任务中调用dispatch_sync函数,而这个任务正在你当作参数传递给dispatch_sync的queue上面在执行。这样做会让queue死锁。如果你需要分发到当前queue,使用dispatch_async函数来异步的处理。
3. 避免在你提交到dispatch queue的任务中使用locks。尽管在你任务中使用锁是安全的,当你获取锁时,如果这个锁不可用时,你就有组赛整个串行队列的风险。类似的,对于并发队列,等待一个锁可能会阻止其他的任务的执行。如果你需要同步你代码的执行,可以使用一个串行dispatch queue来代替锁。
4. 尽管你可以获取底层线程运行一个任务,但是最好避免这样做。
Dispatch Sources
每当你与底层系统交互时,你必须准备花费大量的时间为那项任务做好准备。调用到内核层面或者其他的系统层面涉及到上线的切换,跟这些调用发生在你自己的进行内部来说是相当的昂贵。因为这个原因,很多系统库提供异步接口来允许你的代码提交一个请求到系统然后在这个请求被处理时继续做其他的工作。GCD通过使用blocks和dispatch queue允许你提交你的请求并且返回结果到你的代码来达到这样的行为。
关于Dispatch sources
一个dispatch source是以一个基础数据类型来协调处理特殊的底层系统事件。GCD支持以下类型的dispatch sources:
1. Timer dispatch sources产生定期的通知
2. Signal dispatch sources在一个UNIX signal到达时通知你
3. Descriptor source通知你不同的基于文件和socket的操作,比如:
a。当数据准备好读
b。当它可能写数据时。
c。当文件在文件系统中被删除,移动,改名
d。当文件元信息改变(包括大小,创建时间)
4.process dispatch source通知你进行相关事件,比如:
a。当一个进程退出
b。当进程进行了fork或者exec类型的调用
c。当一个signal到达一个进程
5. Mach port dispatch sources通知你端口相关的事件
6. Custom dispatch sources是你定义并且自己触发
Dispatch sources取代异步回调函数典型的使用是处理系统相关的事件。当你配置一个dispatch source,你指向你希望监视的事件,dispatch queue和用来处理这些事件的代码。你可以使用block对象或者函数来指定你的代码。当一个感兴趣的事件到达时,dispatch source提交你的blocks或者函数到指定的dispatch queue执行。
不象你手动提交到队列的任务,dispatch sources提供为你的应用程序一个连续的事件源。一个dispatch source始终归属它自己的dispatch queue直到你显示的退出它。当归属时,当对应的事件发生时,它提交它关联的任务代码到dispatch queue。一些事件,比如定时器事件,每隔一段时间发生但是只是偶尔的指定条件出现。因为这个原因,dispatch sources retain它们关联的dispatch
queue来阻止它被过早的释放,当事件可能还在继续到来。
为了阻止事件在dispatch queue中积压太多,dispatch sources实现一个事件合并调度。如果一个新事件在上一个事件已经出列了并且被执行还没有执行完时到达,dispatch source合并新事件上的数据和老事件的数据。依赖于事件的类别,合并可能取代老的事件或者更新它保持的信息。比如,一个基于signal的dispatch source只提供关于最近的signal信息,但是同时也报告自从事件处理的上一次调用后一共有多少signals已经被运送。
创建Dispatch Sources
创建一个dispatch source涉及到同时创建事件的资源和dispatch source本身。事件的资源就是处理事件需要的原生的数据结构。比如,一个基于喵舒服的dispatch source,你可能需要打开这个描述符,基于进程的source你可能需要获取目标进程的ID。当你有你的事件资源,你可以创建对应的dispatch source如下:
1. 使用dispatch_source_create函数创建dispatch source
2. 配置dispatch source:
给这个dispatch source赋予一个事件处理器。
对于定时器资源,使用dispatch_source_set_timer函数来设置定时器信息。
3. 可选的赋予一个退出处理器给这个dispatch source。
4. 调用dispatch_resume函数来启动处理事件。
因为dispatch sources在它们被使用前需要一些额外的配置,dispatch_source_create函数返回一个暂停状态的dispatch source。当暂时时,dispatch source接收事件但是不处理它们。这给了你时间来安装一个事件处理器来执行任务额外地配置来处理实际的事件。
接下来的部分将为你介绍如何配置dispatch source不同的方面。
编写和安装一个事件处理器
为了处理dispatch source产生的事件,你必须定义一个事件处理器来处理这些事件。一个事件处理器是一个函数或者一个block对象你使用dispatch_source_set_event_handler或者dispatch_source_set_event_handler_f函数安装到你的dispatch source。当一个事件到达时,这个dispatch source提交你的事件处理器到一个指定的dispatch
queue来处理。
你的事件处理器的主要部分负责处理任何到达的事件。当你的事件处理器已经排队来等待处理一个事件时又来了一个新事件,dispatch source合并这2个事件。一个事件处理器通常来说只能看到最近的事件,但是根据dispatch source的不同,它可能可以获取发生的被合并的其他事件的信息。如果一个或者多个新事件在事件处理器已经开始处理时到达,dispatch source保持这些事件直到当前事件处理器完成执行。这时,它把事件处理器和新的事件提交给queue。
基于函数的事件处理器有一个单独的上下文指针,包含dispatch source对象,没有返回值。基于block的事件处理器没有参数也没有返回值。
// Block-based event handler
void (^dispatch_block_t)(void)
// Function-based event handler
void (*dispatch_function_t)(void *)
void (^dispatch_block_t)(void)
// Function-based event handler
void (*dispatch_function_t)(void *)
在你的事件处理器内部,你可以获取关于dispatch source本身给定的事件的信息。尽管基于函数的事件处理器传递一个执行dispatch source的指针的参数,基于block的事件处理器必须自己捕获这个指针。通常,你可以通过引用包含dispatch source的变量来达到这个目的。如下所示的代码捕获了声明在block范围外的source变量。
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
myDescriptor, 0, myQueue);
dispatch_source_set_event_handler(source, ^{
// Get some data from the source variable, which is captured
// from the parent context.
size_t estimated = dispatch_source_get_data(source);
// Continue reading the descriptor...
});
dispatch_resume(source);
myDescriptor, 0, myQueue);
dispatch_source_set_event_handler(source, ^{
// Get some data from the source variable, which is captured
// from the parent context.
size_t estimated = dispatch_source_get_data(source);
// Continue reading the descriptor...
});
dispatch_resume(source);
在一个block内部捕获变量通常带来极大的灵活性和动态性。当前默认的捕获的变量在block内部是只读的。尽管blocks特性在特定环境下为可修改的捕获变量供了支持,你不应该在一个dispatch source关联的事件处理器中企图这样做。dispatch sources常常异步的执行它们自己的事件处理器,所以任何你捕获的变量的定义范围在你的事件处理器执行时已经消失了。
下表列出了你可以从你的事件处理器中调用的可以用来获取事件的信息的函数
安装一个退出处理器
退出处理器是用来清理一个dispatch source在释放它之前。大部分类型的dispatch sources,退出处理器是可选的,如果你有一些自定义的行为关联到dispatch source需要被更新退出处理器才是必须的。使用描述符或者mach端口的dispatch sources,你必须提供一个退出处理器来关闭描述符或者释放mach端口。没有成功处理的话会导致细微的bug出现在由这些结构无意的被重用被你的代码或者系统的其他部分产生的代码。
你可以在任何时候安装一个退出处理器但是通常你可能会在创建dispatch source时安装。你使用dispatch_source_cancel_handler或者dispatch_source_set_cancel_handler_f函数,取决于在你的实现中是希望使用一个block对象还是函数。下面的例子展示了一个简单的退出处理器来关闭一个已经打开的描述符。fd就是描述符。
dispatch_source_set_cancel_handler(mySource, ^{
close(fd); // Close a file descriptor opened earlier.
});
close(fd); // Close a file descriptor opened earlier.
});
改变目标Queue
当你创建一个dispatch source时,尽管你指定了运行你的事件和退出事件处理器的Queue,你可以使用dispatch_set_target_queue函数在任何时候改变这个queue。你可能这样做来改变dispatch source的已经被处理的事件优先级。
改变一个dispatch source的 queue是一个异步操作,dispatch source尽最大努力来让这个改变尽可能的快。当一个事件处理器已经排队并且等待被处理,它就在之前的队列上执行。然而,在你进行更改的时间附近的其他到达的事件可能在其中任何一个queue上被处理。
关联用户数据到一个dispatch source
就像GCD中其他数据类型一样,你可以使用dispatch_set_context函数来关联用户数据到一个dispatch source。你可以使用这个上下文指针指向你的事件处理器处理事件时需要的任何数据。如果你已经存储了用户数据在一个context指针上,你必须安装一个退出处理器来释放这些数据当dispatch sources不再需要时。
如果你使用blocks实现你的事件处理器,你可以捕获局部变量然后在你的block代码中使用它们。尽管这可能减少存储到dispatch source上下文指针中的数据的需求,你必须经常明确而谨慎地使用这个特性。因为dispatch sources可能在你的应用中长时间的生存,你必须在捕获包含指针的变量时注意。如果数据被一个指针指向可能在任何时间被释放,你必须要么copy这个数据或者retain它来阻止这样的事情发生。在任何一种情况,你可以需要提供一个退出处理器来稍后释放数据。
Dispatch Sources的内存管理
就像其他的dispatch对象一样,dispatch sources是引用计数的数据类型。一个dispatch source有一个初始化为1的引用计数并且可以使用dispatch_retain和dispatch_release函数来retain和release。当queue的引用计数到达0时,西条自动释放dispatch source数据结构。
因为它们的使用方式,dispatch sources的所有权可以被dispatch source自己内部地或者外部地管理。外部的所有权,另外一个对象或者一段代码来管理dispatch source并且在它不再需要时负责释放它。内部的所有权,dispatch source拥有它自己并且在适合的时间负责释放它。尽管外部的关系非常普通,你可以在你希望创建一个自治的dispatch source并且让它自己管理你代码的一些行为不需要更近一步的交互时使用内部的关系。比如,如果一个dispatch source被设计用来响应一个单独的全局事件,你可能用它来处理这个事件然后立即退出。
Dispatch Source Examples
下面的部分将展示如何创建和配置一些通用的dispatch sources的使用。
创建一个定时器(Timer)
定时器dispatch source定期的,基于时间的间隔的产生事件。你可以使用定时器初始化指定的任务需要定期的执行。比如,游戏和其他的图像感知的应用可能使用定时器来初始化屏幕或者动画更新。你也可以设置一个定时器使用使用结果事件来检查新信息在一个频繁更新的服务器。
所有的定时器dispatch sources是间隔定时器。就是一旦创建,它们就以你指定的间隔传递定期的任务。当你创建一个定时器dispatch source,你必须指定的一个值是leeway value来给系统一些关于正确的要求定时器事件的主意。Leeway值给了西条一些灵活性在如何管理电源和唤醒CPU。比如,系统可能使用这个leeway值来提升或者降低触发的时间和用其他的系统事件更好的匹配它。你必须因此尽可能为你的定时器指定一个leeway值。
注意:即使你指定了leeway值为0,你不应该期待一个计时器触发你请求的确切纳秒。系统尽它最大的可能来满足你的请求但是不能保证准确的触发时间。
当一个计算机进行睡眠,所有的定时器dispatch sources都被暂停了。当这个计算机唤醒时,这些定时器dispatch sources也自动的被唤醒。依赖于定时器的配置,暂停的这个性质可能影响你的定时器下一次触发。如果你使用dispatch_time函数或者DISPATCH_TIME_NOW常量来设置你的定时器dispatch sources,这个定时器dispatch sources使用默认的系统时钟来决定什么时候触发。然而,默认的系统时钟在计算机睡眠时不前进。相对而言,当你使用dispatch_walltime函数设置你的定时器dispatch
source,定时器dispatch source跟踪它的触发时间的挂钟时间。这第二个选择通常对间隔相对来说比较大的定时器合适,因为它阻止了有事件时间之间的太多飘移。
下面展示了一个每隔30秒触发一次的定时器,有一个1秒的leeway值。因为时间间隔相当大,dispatch source使用dispatch_walltime函数来创建。定时器的第一次触发立即就发生了然后下一次的触发是每隔30秒。MyPeridicTask和MyStoreTimer符号代表了用户函数你可能需要实现定时器的行为和存储定时器在你的应用的数据结构中。
dispatch_source_t CreateDispatchTimer(uint64_t interval,
uint64_t leeway,
dispatch_queue_t queue,
dispatch_block_t block)
{
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, queue);
if (timer) {
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval,
leeway);
dispatch_source_set_event_handler(timer, block);
dispatch_resume(timer);
}
return timer;
}
uint64_t leeway,
dispatch_queue_t queue,
dispatch_block_t block)
{
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, queue);
if (timer) {
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval,
leeway);
dispatch_source_set_event_handler(timer, block);
dispatch_resume(timer);
}
return timer;
}
void MyCreateTimer()
{
dispatch_source_t aTimer = CreateDispatchTimer(30ull * NSEC_PER_SEC,
1ull * NSEC_PER_SEC,
dispatch_get_main_queue(),
^{ MyPeriodicTask(); });
// Store it somewhere for later use.
if (aTimer)
{
MyStoreTimer(aTimer);
}
}
{
dispatch_source_t aTimer = CreateDispatchTimer(30ull * NSEC_PER_SEC,
1ull * NSEC_PER_SEC,
dispatch_get_main_queue(),
^{ MyPeriodicTask(); });
// Store it somewhere for later use.
if (aTimer)
{
MyStoreTimer(aTimer);
}
}
尽管创建一个定时器dispatch source是主要的方式来接收基于时间的事件,还有其他的选择。如果你希望在一个指定的时间间隔后执行一个block一次,你可以使用dispatch_after或者dispatch_after_f函数。这个函数工作得像dispatch_async函数除了它允许你指定一个时间值哪个提交block到一个queue上。根据你的代码这个时间值可以相对的或者决定值被指定。
从一个描述符读取数据
从一个文件或者socket读取数据,你必须打开文件或者socket然后创建类型是DISPATCH_SOURCE_TYPE_READ的dispatch source。这个你指定的事件处理器必须有能力读取和处理文件描述符的内容。如果是文件,这包括读取文件的数据和创建合适的数据结构。如故是一个网络socket,这涉及到处理新接收到的网络数据。
当读取数据时,你必须总是使用非阻塞的操作来配置你的描述符。尽管你可以使用dispatch_source_get_data函数来看有多少可读的数据,这个函数的返回数目可能会在你调用这个函数和你真的读取数据之间的时候内改变。如果这个文件被删除了或者网络出现了错误,从描述符读数据会阻塞当前线程,可能在执行过程中停止你的事件处理器并且阻止dispatch queue分发其他的任务。对于一个串行队列,这可能会让你的queue死锁,对于一个并发队列可能会减少可以被开始执行的新任务的数量。
下面的例子展示了配置一个dispatch source来读取一个文件的数据。在这个例子中,事件处理器读取整个文件的内容到一个buffer中,然后调用用户函数来处理这些数据。为了确保这个dispatch queue不会被没有必要的阻塞当没有数据读取时,这个例子使用了fcntl函数来配置这个文件描述符来执行非阻塞的操作。退出处理器在dispatch source的安排确保数据被读取后文件描述符被关闭。
dispatch_source_t ProcessContentsOfFile(const char*
filename)
{
// Prepare the file for reading.
int fd = open(filename, O_RDONLY);
if (fd == -1)
return NULL;
fcntl(fd, F_SETFL, O_NONBLOCK); // Avoid blocking the read operation
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, queue);
if (!readSource)
{
close(fd);
return NULL;
}
// Install the event handler
dispatch_source_set_event_handler(readSource, ^{
size_t estimated = dispatch_source_get_data(readSource) + 1;
// Read the data into a text buffer.
char* buffer = (char*)malloc(estimated);
if (buffer)
{
ssize_t actual = read(fd, buffer, (estimated));
Boolean done = MyProcessFileData(buffer, actual); // Process the data.
// Release the buffer when done.
free(buffer);
// If there is no more data, cancel the source.
if (done)
dispatch_source_cancel(readSource);
} });
// Install the cancellation handler
dispatch_source_set_cancel_handler(readSource, ^{close(fd);});
// Start reading the file.
dispatch_resume(readSource);
return readSource;
}
{
// Prepare the file for reading.
int fd = open(filename, O_RDONLY);
if (fd == -1)
return NULL;
fcntl(fd, F_SETFL, O_NONBLOCK); // Avoid blocking the read operation
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, queue);
if (!readSource)
{
close(fd);
return NULL;
}
// Install the event handler
dispatch_source_set_event_handler(readSource, ^{
size_t estimated = dispatch_source_get_data(readSource) + 1;
// Read the data into a text buffer.
char* buffer = (char*)malloc(estimated);
if (buffer)
{
ssize_t actual = read(fd, buffer, (estimated));
Boolean done = MyProcessFileData(buffer, actual); // Process the data.
// Release the buffer when done.
free(buffer);
// If there is no more data, cancel the source.
if (done)
dispatch_source_cancel(readSource);
} });
// Install the cancellation handler
dispatch_source_set_cancel_handler(readSource, ^{close(fd);});
// Start reading the file.
dispatch_resume(readSource);
return readSource;
}
在上面的例子中,用户的MyProcessFileData函数决定何时足够的文件数据被读取然后dispatch source就可以被退出了。默认的,一个dispatch source配置来读取一个描述符,重复地调度它的事件处理器当有数据可以被读取时。如果这个socket连接关闭了或者你到达了文件地最后,这个dispatch
source自动地停止调度事件处理器。如果你知道你不需要一个dispatch source,你可以直接退出它。
写数据到描述符
处理写入数据到一个文件或者socket跟处理读数据非常的相似。为一个描述符配置完写操作配置以后,你创建一个类型是DISPATCH_SOURCE_TYPE_WRITE的dispatch source。一旦这个dispatch source创建以后,系统调用你的事件处理器来给他一个机会开始写入数据到文件或者socket。当你完成了写数据以后,使用dispatch_source_cancel函数来退出这个dispatch source。
当你写数据时,你必须总是配置你的文件描述符使用非阻塞的操作。尽管你可以使用dispatch_source_get_data函数来看有多少空间可以用来写,这个函数的返回值只是一个建议值并且在你调用这个函数的时间和你实际写数据的时间之间发生改变。如果一个错误发生了,写入数据到一个阻塞的文件描述符会可能在执行过程中停止你的事件处理器并且阻止dispatch queue分发其他的任务。对于一个串行队列,这可能会让你的queue死锁,对于一个并发队列可能会减少可以被开始执行的新任务的数量。
下面的例子展示了基本的方法使用dispatch source来写入一个文件。创建一个新文件以后,这个函数传递产生的文件描述符到它的事件处理器。数据写入到文件是由MyGetData函数提供的,你可以替代任何代码你需要的来为这个文件生产数据。在写入数据到文件以后,这个事件处理器退出这个dispatch source来阻止它被再次调用。dispatch source的拥有者可能这时有责任释放它。
dispatch_source_t WriteDataToFile(const char*
filename)
{
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC,
(S_IRUSR | S_IWUSR | S_ISUID | S_ISGID));
if (fd == -1)
return NULL;
fcntl(fd, F_SETFL); // Block during the write.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE,fd, 0, queue);
if (!writeSource)
{
close(fd);
return NULL;
}
dispatch_source_set_event_handler(writeSource, ^{
size_t bufferSize = MyGetDataSize();
void* buffer = malloc(bufferSize);
size_t actual = MyGetData(buffer, bufferSize);
write(fd, buffer, actual);
free(buffer);
// Cancel and release the dispatch source when done.
dispatch_source_cancel(writeSource);
});
dispatch_source_set_cancel_handler(writeSource, ^{close(fd);});
dispatch_resume(writeSource);
return (writeSource);
}
{
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC,
(S_IRUSR | S_IWUSR | S_ISUID | S_ISGID));
if (fd == -1)
return NULL;
fcntl(fd, F_SETFL); // Block during the write.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE,fd, 0, queue);
if (!writeSource)
{
close(fd);
return NULL;
}
dispatch_source_set_event_handler(writeSource, ^{
size_t bufferSize = MyGetDataSize();
void* buffer = malloc(bufferSize);
size_t actual = MyGetData(buffer, bufferSize);
write(fd, buffer, actual);
free(buffer);
// Cancel and release the dispatch source when done.
dispatch_source_cancel(writeSource);
});
dispatch_source_set_cancel_handler(writeSource, ^{close(fd);});
dispatch_resume(writeSource);
return (writeSource);
}
监视一个文件系统对象
如果你希望监视一个文件系统对象的更改,你可以设置一个类型为DISPATCH_SOURCE_TYPE_VNODE的dispatch source。你可以使用这个类型的dispatch source在文件被删除,写入,重命名时接收通知。你也可以使用它进行报警当文件的指定类型的元数据(size and link count)更改了。
Note:你为你的dispatch source指定的文件描述符必须在处理事件时始终打开。
下面展示了监视文件名字改变和当发生改变时执行一些用户行为。因为一个描述符明确的为一个dispatch source打开,这个dispatch source包括一个退出处理器来关闭这个描述符。因为例子创建的这个文件描述符是关联的基本的文件系统对象,这个dispatch source可以被用来检测任何数据的文件名的更改。
dispatch_source_t MonitorNameChangesToFile(const char*
filename)
{
int fd = open(filename, O_EVTONLY);
if (fd == -1)
return NULL;
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE,
fd, DISPATCH_VNODE_RENAME, queue);
if (source) {
// Copy the filename for later use.
int length = strlen(filename);
char* newString = (char*)malloc(length + 1);
newString = strcpy(newString, filename);
dispatch_set_context(source, newString);
// Install the event handler to process the name change
dispatch_source_set_event_handler(source, ^{
const char* oldFilename = (char*)dispatch_get_context(source);
MyUpdateFileName(oldFilename, fd);
});
// Install a cancellation handler to free the descriptor
// and the stored string.
dispatch_source_set_cancel_handler(source, ^{
char* fileStr = (char*)dispatch_get_context(source);
free(fileStr);
close(fd);
});
// Start processing events.
dispatch_resume(source);
}
else
close(fd);
return source;
}
{
int fd = open(filename, O_EVTONLY);
if (fd == -1)
return NULL;
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE,
fd, DISPATCH_VNODE_RENAME, queue);
if (source) {
// Copy the filename for later use.
int length = strlen(filename);
char* newString = (char*)malloc(length + 1);
newString = strcpy(newString, filename);
dispatch_set_context(source, newString);
// Install the event handler to process the name change
dispatch_source_set_event_handler(source, ^{
const char* oldFilename = (char*)dispatch_get_context(source);
MyUpdateFileName(oldFilename, fd);
});
// Install a cancellation handler to free the descriptor
// and the stored string.
dispatch_source_set_cancel_handler(source, ^{
char* fileStr = (char*)dispatch_get_context(source);
free(fileStr);
close(fd);
});
// Start processing events.
dispatch_resume(source);
}
else
close(fd);
return source;
}
监视Signals
UNIX信号允许来自其领域之外的应用程序的操作。一个应用程序可以接受许多不同类型的signals包括不可恢复的错误来通知重要的信息(比如一个子进程推出了)。传统地,应用使用sigaction函数来安装一个signal处理器函数,来同步的处理signals。如果你仅仅是希望一个signal达时被通知而不是实际的希望处理这个signal,你可以使用一个signal dispatch source来异步的处理signals。
signal dispatch source不是你使用sigaction函数安装同步signal处理器的替代物。同步信号处理器可以实际捕获信号量然后阻止它终止你的应用。signal dispatch source 允许你监视到达的signal。除此之外,你不能使用signal dispatch source检索其他类型的signals。特别的,你不能使用它们来监视SIGILL,SIGBUS,SIGSEGV信号。
因为signal dispatch source在dispatch queue 上被异步的执行,它们不受同步信号处理器的一些同样的限制。比如,你可以从你的signal dispatch source’s 事件处理器上调用函数这是没有限制的。事实上这增加了灵活性,可能会增加一些延迟在signal事件到达的时间和你的dispatch source’s event处理器被调用之间。
下面展示你如何配置一个signal dispatch source来处理SIGHUP signal。dispatch source的事件处理器调用MyProcessSIGHUP函数,你可以在你的应用中替换来处理signal。
void InstallSignalHandler()
{
// Make sure the signal does not terminate the application.
signal(SIGHUP, SIG_IGN);
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL,
SIGHUP, 0, queue);
if (source) {
dispatch_source_set_event_handler(source, ^{
MyProcessSIGHUP();
});
// Start processing signals
dispatch_resume(source);
}
}
{
// Make sure the signal does not terminate the application.
signal(SIGHUP, SIG_IGN);
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL,
SIGHUP, 0, queue);
if (source) {
dispatch_source_set_event_handler(source, ^{
MyProcessSIGHUP();
});
// Start processing signals
dispatch_resume(source);
}
}
监视一个进程
一个进程dispatch source让你监视指定进程的行为和正确的响应。一个父进程可能使用这种类型的dispatch source来监视任何它创建的子进程。比如,父进程可以使用它来观察子进程的结束。类似的,一个子进程可以使用它来监视父进去然而在父进程结束的时候结束。
下面的步骤展示安装一个dispatch source来监视一个父进程的终止。当父进程终止了,dispatch source设置一些内部的状态信息来让子进程知道应该退出了(你自己的应用可能需要实现MySetAppExitFlag函数来设置合适的终止标志)。因为dispatch source自治的运行,因此它拥有它自己,它退出和释放自己在应用关闭之前。
void MonitorParentProcess()
{
pid_t parentPID = getppid();
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
parentPID, DISPATCH_PROC_EXIT,
queue);
if (source) {
dispatch_source_set_event_handler(source, ^{
MySetAppExitFlag();
dispatch_source_cancel(source);
dispatch_release(source);
});
dispatch_resume(source);
}
}
{
pid_t parentPID = getppid();
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
parentPID, DISPATCH_PROC_EXIT,
queue);
if (source) {
dispatch_source_set_event_handler(source, ^{
MySetAppExitFlag();
dispatch_source_cancel(source);
dispatch_release(source);
});
dispatch_resume(source);
}
}
退出一个Dispatch Source
Dispatch Source始终保持活动直到你使用dispatch_source_cancel函数显示地退出它们。退出一个dispatch source停止新事件的传递并且不能被完成。因此,你典型的退出一个dispatch source然后立即释放它们。
void RemoveDispatchSource(dispatch_source_t mySource)
{
dispatch_source_cancel(mySource);
dispatch_release(mySource);
}
{
dispatch_source_cancel(mySource);
dispatch_release(mySource);
}
退出一个dispatch source是一个异步操作。尽管在你调用dispatch_source_cancel函数后没有新的事件被处理,事件已经被dispatch source处理的可以继续被执行。当它处理所有最后的事件,dispatch source执行它的退出处理器如果它有的话。
退出处理器一个你释放内存或者清理任何资源的机会。如果你的dispatch source使用一个描述符或者一个mach端口,在退出时你必须提供一个退出处理器来关闭描述符或者销毁端口。其他类型的dispatch sources不需要退出处理器,尽管你仍然应用提供一个如果在你的dispatch source上你关联了任何内存和数据。比如,你必须提供一个如果你存储数据在dispatch source’s的上下文指针。
暂时和恢复dispatch sources
你可以使用dispatch_suspend和dispatch_resume来暂时的暂停和恢复dispatch source事件的传递。这些方法增加和减少你dispatch对象的暂时计数。你必须平衡每个dispatch_suspend调用dispatch_resume在任何事件传递恢复之前。
当你暂停一个dispatch source,在dispatch source被暂停时任何发生的事件都被累计直到这个queue恢复。当这个queu恢复以后,事件会合并成一个单独的事件而不是传递所有的事件。比如,如果你监视一个文件的名字的更改,被传递的事件可能只包括最后一次名字的更改。这种方式合并事件可以阻止它们在queue中聚集导致当工作恢复时压垮了你的应用。