Grand Central Dispatch(GCD)调度队列是执行任务的强大工具。 通过调度队列,您可以相对于调用者异步或同步地执行任意代码块。 您可以使用调度队列执行几乎所有在单独线程上执行的任务。 调度队列的优点是它们比相应的线程代码更易于使用,并且执行这些任务的效率更高。
本章介绍了调度队列,以及有关如何使用它们在应用程序中执行常规任务的信息。 如果要使用调度队列替换现有的线程代码,可以在迁移线程(Migrating Away from Threads)中找到一些有关如何执行此操作的其他提示。
调度队列是在应用程序中异步和并发执行任务的简便方法。 任务只是您的应用程序需要执行的一些工作。 例如,您可以定义任务以执行某些计算,创建或修改数据结构,处理从文件读取的某些数据或任何数量的事物。 您可以通过将相应的代码放在函数或块对象中并将其添加到调度队列来定义任务。
调度队列是一种类似于对象的结构,用于管理您提交给它的任务。 所有调度队列都是先进先出的数据结构。 因此,添加到队列的任务始终按照添加的顺序启动。 GCD会自动为您提供一些调度队列,但您可以为特定目的创建其他调度队列。 表3-1列出了应用程序可用的调度队列类型以及如何使用它们。
类型 |
描述 |
---|---|
串行 |
串行队列(也称为专用调度队列)按照将它们添加到队列的顺序一次执行一个任务。当前正在执行的任务在由调度队列管理的不同线程(可能因任务而异)上运行。串行队列通常用于同步对特定资源的访问。 您可以根据需要创建任意数量的串行队列,并且每个队列与所有其他队列同时运行。换句话说,如果您创建四个串行队列,则每个队列一次只执行一个任务,但最多可以同时执行四个任务,每个队列一个。有关如何创建串行队列的信息,请参阅创建串行调度队列。 |
同时 |
并发队列(也称为一种全局调度队列)同时执行一个或多个任务,但任务仍按其添加到队列的顺序启动。当前正在执行的任务在由调度队列管理的不同线程上运行。在任何给定点执行的任务的确切数量是可变的,取决于系统条件。 在iOS 5及更高版本中,您可以通过指定 |
主调度队列 |
主调度队列是一个全局可用的串行队列,它在应用程序的主线程上执行任务。此队列与应用程序的运行循环(如果存在)一起工作,以将排队任务的执行与附加到运行循环的其他事件源的执行交错。因为它在应用程序的主线程上运行,所以主队列通常用作应用程序的关键同步点。 虽然您不需要创建主调度队列,但您需要确保应用程序正确地消耗它。有关如何管理此队列的更多信息,请参阅在主线程上执行任务。 |
在为应用程序添加并发性时,调度队列提供了多个优于线程的优点。最直接的优点是工作队列编程模型的简单性。使用线程,您必须为要执行的工作以及线程本身的创建和管理编写代码。调度队列使您可以专注于您实际想要执行的工作,而无需担心线程的创建和管理。相反,系统会为您处理所有线程创建和管理。优点是系统能够比任何单个应用程序更有效地管理线程。系统可以根据可用资源和当前系统条件动态扩展线程数。此外,
虽然您可能认为重写代码队列的代码很困难,但为代码队列编写代码通常比编写线程代码更容易。编写代码的关键是设计自包含且能够异步运行的任务。(这对于线程和调度队列都是如此。)但是,调度队列具有优势的可预测性。如果您有两个访问相同共享资源但在不同线程上运行的任务,则任一线程都可以先修改资源,您需要使用锁来确保两个任务不会同时修改该资源。使用调度队列,您可以将两个任务添加到串行调度队列,以确保在任何给定时间只有一个任务修改了资源。
虽然您指出在串行队列中运行的两个任务不会同时运行是正确的,但您必须记住,如果两个线程同时进行锁定,则线程提供的任何并发都将丢失或显着减少。更重要的是,线程模型需要创建两个线程,这两个线程占用内核和用户空间内存。调度队列不会为其线程支付相同的内存损失,并且它们使用的线程会保持忙碌而不会被阻塞。
关于调度队列需要记住的其他一些关键点包括:
调度队列与其他调度队列同时执行其任务。任务的序列化仅限于单个调度队列中的任务。
系统确定任何时间执行的任务总数。因此,在100个不同队列中具有100个任务的应用程序可能不会同时执行所有这些任务(除非它具有100个或更多有效核心)。
在选择要启动的新任务时,系统会考虑队列优先级。有关如何设置串行队列优先级的信息,请参阅为队列提供清理功能。
队列中的任务必须准备好在添加到队列时执行。(如果您之前使用过Cocoa operation object,请注意此行为与operation使用的 模型不同。)
private dispatch queues是引用计数对象。除了在自己的代码中保留队列之外,请注意,dispatch sources也可以附加到队列,并增加其保留计数。因此,您必须确保取消所有dispatch sources,并使用适当的释放调用平衡所有保留调用。有关保留和释放队列的详细信息,请参阅调度队列的内存管理。有关调度源的更多信息,请参阅关于调度源。
有关用于操作调度队列的接口的更多信息,请参阅Grand Central Dispatch(GCD)参考。
除了调度队列之外,Grand Central Dispatch还提供了几种使用队列来帮助管理代码的技术。表3-2列出了这些技术,并提供了指向您可以在哪里找到有关它们的更多信息的链接。
技术 |
描述 |
---|---|
Dispatch groups |
调度组是一种监视一组块对象以完成的方法。(您可以根据需要同步或异步监视块。)组为代码提供了有用的同步机制,这取决于其他任务的完成。有关使用组的详细信息,请参阅等待排队任务组。 |
Dispatch semaphores |
调度信号量类似于传统信号量,但通常更有效。仅当因为信号量不可用而需要阻塞调用线程时,Dispatch信号量才会调用内核。如果信号量可用,则不进行内核调用。有关如何使用调度信号量的示例,请参阅使用调度信号量来调节有限资源的使用。 |
Dispatch sources |
调度源生成通知以响应特定类型的系统事件。您可以使用调度源来监视诸如进程通知,信号和描述符事件等事件。发生事件时,调度源将您的任务代码异步提交到指定的调度队列以进行处理。有关创建和使用调度源的更多信息,请参阅调度源。 |
搜索除了调度队列之外,Grand Central Dispatch还提供了几种使用队列来帮助管理代码的技术。表3-2列出了这些技术,并提供了指向您可以在哪里找到有关它们的更多信息的链接。
技术 |
描述 |
---|---|
派遣小组 |
调度组是一种监视一组块对象以完成的方法。(您可以根据需要同步或异步监视块。)组为代码提供了有用的同步机制,这取决于其他任务的完成。有关使用组的详细信息,请参阅等待排队任务组。 |
发送信号量 |
调度信号量类似于传统信号量,但通常更有效。仅当因为信号量不可用而需要阻塞调用线程时,Dispatch信号量才会调用内核。如果信号量可用,则不进行内核调用。有关如何使用调度信号量的示例,请参阅使用调度信号量来调节有限资源的使用。 |
派遣来源 |
调度源生成通知以响应特定类型的系统事件。您可以使用调度源来监视诸如进程通知,信号和描述符事件等事件。发生事件时,调度源将您的任务代码异步提交到指定的调度队列以进行处理。有关创建和使用调度源的更多信息,请参阅调度源。 |
块对象是基于C的语言特性,可以在C, Objective-C和C ++代码中使用。块可以轻松定义独立的工作单元。虽然它们看起来类似于函数指针,但实际上块是由类似于对象的底层数据结构表示的,并由编译器为您创建和管理。编译器打包您提供的代码(以及任何相关数据),并将其封装在可以存放在堆中并在应用程序中传递的模式中。
块的一个关键优势是它们能够在自己的语法范围之外使用变量。在函数或方法中定义块时,块在某些方面将充当传统的代码块。例如,块可以读取父作用域中定义的变量的值。块访问的变量将复制到堆上的块数据结构中,以便块稍后可以访问它们。将块添加到调度队列时,这些值通常必须保留为只读格式。但是,同步执行的块也可以使用具有__block
前置关键字的变量将数据返回给父调用范围。
使用与函数指针使用的语法类似的语法声明与代码内联的块。块和函数指针之间的主要区别在于块名称前面带有插入符号(^
)而不是星号(*
)。像函数指针一样,您可以将参数传递给块并从中接收返回值。清单3-1展示了如何在代码中同步声明和执行块。该变量aBlock
被声明为一个块,它接受一个整数参数并且不返回任何值。然后将匹配该原型的实际块分配给aBlock
内联并声明。最后一行立即执行块,将指定的整数打印到标准输出。
清单3-1 一个简单的块示例
int x = 123;
int y = 456;
//块声明和赋值
void(^ aBlock)(int)= ^(int z){
printf(“%d%d%d \ n”,x,y,z);
};
//执行块
ABLOCK(789); //打印:123 456 789
以下是设计块时应考虑的一些关键指南的摘要:
1.对于计划使用调度队列异步执行的块,可以安全地从父函数或方法捕获标量变量,并在块中使用它们。但是,您不应尝试捕获由调用上下文分配和删除的大型结构或其他基于指针的变量。在块执行时,该指针引用的内存可能会消失。当然,自己分配内存(或对象)并明确地将该内存的所有权移交给块是安全的。
2. Dispatch队列复制添加到它们的块,并在它们完成执行时释放块。换句话说,在将块添加到队列之前,您不需要显式复制块。
3. 尽管队列在执行小任务时比原始线程更有效,但仍然存在创建块并在队列上执行它们的开销。如果一个块工作太少,那么内联执行它可能比将它发送到队列更合算。判断块是否工作量太少的方法是使用性能工具收集每个路径的度量标准并进行比较。
4.不要缓存相对于底层线程的数据,并期望可以从不同的块访问数据。如果同一队列中的任务需要共享数据,请使用调度队列的上下文指针来存储数据。有关如何访问调度队列的上下文数据的更多信息,请参阅使用队列存储自定义上下文信息Storing Custom Context Information with a Queue。
5.如果您的块创建了多个Objective-C对象,您可能希望将块的代码部分包含在@autorelease块中,以处理这些对象的内存管理。尽管GCD调度队列具有自己的自动释放池,但它们无法保证这些池何时耗尽。如果您的应用程序受内存限制,则创建自己的自动释放池允许您以更加规则的间隔释放自动释放对象的内存。
有关块的更多信息,包括如何声明和使用它们,请参阅Blocks Programming Topics.。有关如何将块添加到调度队列的信息,请参阅向队列添加任务Adding Tasks to a Queue。
在将任务添加到队列之前,必须确定要使用的队列类型以及打算如何使用它。调度队列可以串行或并行执行任务。此外,如果您特意使用队列,则可以相应地配置队列属性。以下部分介绍如何创建调度队列并配置它们以供使用。
当您有多个可以并行运行的任务时,并发调度队列非常有用。并发队列仍然是队列,因为它以先进先出顺序将任务队列化; 但是,并发队列可能会在任何先前任务完成之前将其他任务出列。并发队列在任何给定时刻执行的实际任务数是可变的,并且可以随着应用程序中的条件的变化而动态变化。许多因素会影响并发队列执行的任务数,包括可用核心数,其他进程正在完成的工作量以及其他串行调度队列中任务的数量和优先级。
系统为每个应用程序提供四个并发调度队列。这些队列对于应用程序是全局的,并且仅通过其优先级来区分。因为它们是全局的,所以不要显式创建它们。相反,您使用dispatch_get_global_queue
函数来获取其中一个队列,如以下示例所示:
|
除了获取默认并发队列之外,您还可以通过将DISPATCH_QUEUE_PRIORITY_HIGH
和DISPATCH_QUEUE_PRIORITY_LOW
常量传递给函数来获取具有高优先级和低优先级的队列,或者通过传递常量来获取后台队列DISPATCH_QUEUE_PRIORITY_BACKGROUND
。正如您所料,高优先级并发队列中的任务在默认队列和低优先级队列中的任务之前执行。同样,默认队列中的任务在低优先级队列中的任务之前执行。
注意: 该dispatch_get_global_queue
函数的第二个参数保留用于将来的扩展。现在,你应该总是传递0
这个参数。
虽然调度队列是引用计数对象,但您不需要保留和释放全局并发队列。因为它们对您的应用程序是全局的,所以忽略对这些队列的保留和释放调用。因此,您不需要存储对这些队列的引用。你只需要调用dispatch_get_global_queue来获取它们。
当您希望任务按特定顺序执行时,串行队列非常有用。串行队列一次只执行一个任务,并始终从队列的头部提取任务。您可以使用串行队列而不是锁来保护共享资源或可变数据结构。与锁不同,串行队列确保以可预测的顺序执行任务。只要您将任务异步提交到串行队列,队列就永远不会死锁。
与为您创建的并发队列不同,您必须显式创建和管理要使用的任何串行队列。您可以为应用程序创建任意数量的串行队列,但应避免创建大量串行队列,以此作为同时执行尽可能多的任务的方法。如果要同时执行大量任务,请将它们提交到其中一个全局并发队列。创建串行队列时,请尝试确定每个队列的用途,例如保护资源或同步应用程序的某些关键行为。
清单3-2显示了创建自定义串行队列所需的步骤。该dispatch_queue_create
函数有两个参数:队列名称和一组队列属性。调试器和性能工具显示队列名称,以帮助您跟踪任务的执行方式。第二个参数保留供将来使用,应该是NULL
。
清单3-2 创建一个新的串行队列
|
|
除了您创建的任何自定义队列之外,系统还会自动创建一个串行队列并将其绑定到应用程序的主线程。有关获取主线程队列的更多信息,请参阅运行时获取常见队列。
Grand Central Dispatch提供的功能允许您从应用程序访问多个常见的调度队列:
使用该dispatch_get_current_queue
函数进行调试或测试当前队列的标识。从块对象内部调用此函数将返回块已提交到的队列(现在可能正在运行该队列)。从块外部调用此函数将返回应用程序的默认并发队列。
使用该dispatch_get_main_queue
函数获取与应用程序主线程关联的串行调度队列。此队列是为Cocoa应用程序以及在主线程上调用该dispatch_main
函数或配置运行循环(使用CFRunLoopRef
类型或NSRunLoop
对象)的应用程序自动创建的。
使用该dispatch_get_global_queue
函数获取任何共享并发队列。有关更多信息,请参阅获取全局并发调度队列。
调度队列和其他调度对象是引用计数数据类型。创建串行调度队列时,它的初始引用计数为1.您可以使用dispatch_retain
和dispatch_release
函数根据需要递增和递减该引用计数。当队列的引用计数达到零时,系统异步释放队列。
保留和释放调度对象(例如队列)非常重要,以确保它们在使用时保留在内存中。与内存管理的 Cocoa对象一样,一般规则是如果您计划使用传递给代码的队列,则应在使用之前保留队列,并在不再需要时释放它。这种基本模式可确保队列在您使用时保留在内存中。
注意: 您无需保留或释放任何全局调度队列,包括并发调度队列或主调度队列。任何保留或释放队列的尝试都将被忽略。
即使您实现了垃圾收集的应用程序,您仍必须保留并释放调度队列和其他调度对象。Grand Central Dispatch不支持回收内存的垃圾收集模型。
所有调度对象(包括调度队列)都允许您将自定义上下文数据与对象相关联。要在给定对象上设置和获取此数据,请使用dispatch_set_context
和dispatch_get_context
函数。系统不以任何方式使用您的自定义数据,您可以在适当的时间分配和取消分配数据。
对于队列,您可以使用上下文数据存储指向Objective-C对象或其他数据结构的指针,以帮助识别队列或其对代码的预期用途。您可以使用队列的终结器函数在取消分配之前从队列中释放(或取消关联)您的上下文数据。清单3-3中显示了如何编写清除队列上下文数据的终结器函数的示例。
创建串行调度队列后,可以附加终结器函数以在销毁分配队列时执行任何自定义清理。调度队列是引用计数对象,您可以使用该dispatch_set_finalizer_f
函数指定当队列的引用计数达到零时要执行的函数。您可以使用此函数来清除与队列关联的上下文数据,并且仅当上下文指针不是NULL
时才调用该函数
。
清单3-3显示了一个自定义终结器函数和一个创建队列并安装该终结器的函数。队列使用终结器函数来释放存储在队列的上下文指针中的数据。(从代码引用的函数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);
dispatch_set_context(serialQueue, data);
dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);
return serialQueue;
}
要执行任务,必须将其分派到适当的调度队列。您可以同步或异步分派任务,也可以单独或成组分派任务。一旦进入队列,队列将负责尽快执行任务,因为它具有约束和队列中已有的任务。本节向您展示了将任务分派到队列的一些技术,并描述了每个技术的优点。
有两种方法可以将任务添加到队列中:异步或同步。如果可能,使用dispatch_async
和dispatch_async_f
函数的异步执行优先于同步备选。将块对象或函数添加到队列时,无法知道该代码何时执行。因此,异步添加块或函数可以让您安排代码的执行,并继续从调用线程执行其他工作。如果您从应用程序的主线程调度任务(可能是为了响应某些用户事件),这一点尤为重要。
虽然您应尽可能异步添加任务,但有时可能需要同步添加任务以防止竞争条件或其他同步错误。在这些实例中,您可以使用dispatch_sync
和dispatch_sync_f
函数将任务添加到队列中。这些函数会阻塞当前执行的线程,直到指定的任务完成执行。
要点: 永远不要从正在计划传递该函数给同一队列中执行的任务中调用dispatch_sync和
dispatch_sync_f
函数。这对于保证死锁的串行队列尤其重要,但对于并发队列也应该避免。
以下示例显示如何使用基于块的变体异步和同步分派任务
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");
就其性质而言,调度到队列的任务独立于创建它们的代码而运行。但是,当任务完成后,您的应用程序可能仍希望收到有关该事实的通知,以便它可以合并结果。使用传统的异步编程,您可以使用回调机制来执行此操作,但是使用调度队列可以使用完成块。
完成块只是您在原始任务结束时分派到队列的另一段代码。调用代码通常在完成任务时将完成块作为参数提供。所有任务代码必须做的是在完成其工作时将指定的块或函数提交到指定的队列。
清单3-4显示了使用块实现的averaging函数。averaging函数的最后两个参数允许调用者指定报告结果时要使用的队列和块。在averaging函数计算其值后,它会将结果传递给指定的块并将其分派到队列中。为防止队列过早释放,最初保留该队列并在分派完成块后释放该队列至关重要。
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);
});
}
并发调度队列可以提高性能的一个地方是您有一个执行固定迭代次数的循环。例如,假设您有一个for
循环,它通过每个循环迭代执行一些操作:
for (i = 0; i < count; i++) {
printf("%u\n",i);
}
如果在每次迭代期间执行的工作与在所有其他迭代期间执行的工作不同,并且每个连续循环完成的顺序不重要,则可以使用对dispatch_apply
or dispatch_apply_f
函数的调用替换循环。对于每个循环迭代,这些函数将指定的块或函数提交给队列一次。当分派到并发队列时,可以同时执行多个循环迭代。
你可以指定一个串行队列或并发队列当调用dispatch_apply
或dispatch_apply_f函数时
。传入并发队列允许您同时执行多个循环迭代,这是使用这些函数的最常用方法。虽然允许使用串行队列并为您的代码做正确的事情,但使用这样的队列与将循环留在原位相比没有真正的性能优势。
重要说明: 与常规for
循环一样,在所有循环迭代完成之前dispatch_apply
,dispatch_apply_f
函数不会返回。因此,从已经从队列上下文执行的代码中调用它们时应该小心。如果作为参数传递给函数的队列是串行队列,并且与执行当前代码的队列相同,则调用这些函数将使队列死锁。
因为它们有效地阻塞了当前线程,所以在从主线程调用这些函数时也应该小心,它们可能会阻止事件处理循环及时响应事件。如果您的循环代码需要大量的处理时间,您可能希望从不同的线程调用这些函数。
清单3-5显示了如何使用dispatch_apply
语法替换前面的for循环。传入dispatch_apply
函数的块必须包含一个标识当前循环迭代的参数。执行块时,此参数的值0
用于第一次迭代,1
第二次迭代,依此类推。最后一次迭代的参数值是count - 1
,其中count
是迭代总数。
清单3-5并发
执行for
循环的迭代
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(count, queue, ^(size_t i) {
printf("%u\n",i);
});
应该确保您的任务代码在每次迭代中都能完成合理的工作量。与调度到队列的任何块或函数一样,调度该代码以执行也会产生开销。如果循环的每次迭代只执行少量工作,则调度代码的开销可能会超过将其分配到队列时可能获得的性能优势。如果您在测试期间发现这是真的,则可以使用跨步来增加每次循环迭代期间执行的工作量。通过跨步,您可以将原始循环的多次迭代组合到一个块中,并按比例减少迭代次数。例如,如果您最初执行100次迭代但决定使用4的步幅,则现在从每个块执行4次循环迭代,并且迭代计数为25。改进循环码。
Grand Central Dispatch提供了一个特殊的调度队列,您可以使用该队列在应用程序的主线程上执行任务。此队列是为所有应用程序自动提供的,并由任何在其主线程上设置运行循环(由CFRunLoopRef
类型或NSRunLoop
对象管理)的应用程序自动排空。如果您没有创建Cocoa应用程序并且不想显式设置运行循环,则必须调用该dispatch_main
函数以显式排空主调度队列。您仍然可以向队列添加任务,但如果不调用此函数,则永远不会执行这些任务。
您可以通过调用该dispatch_get_main_queue
函数来获取应用程序主线程的调度队列。添加到此队列的任务在主线程本身上串行执行。因此,您可以将此队列用作应用程序其他部分中正在完成的工作的同步点。
GCD为Cocoa内存管理技术提供内置支持,因此您可以在调度队列提交的块中自由使用Objective-C对象。每个调度队列都维护自己的自动释放池,以确保在某个时刻释放自动释放的对象; 队列无法保证何时实际释放这些对象。
如果您的应用程序受内存限制且您的块创建了多个自动释放的对象,则创建自己的自动释放池是确保及时释放对象的唯一方法。如果您的块创建了数百个对象,则可能需要创建多个自动释放池或定期排空释放池。
有关自动释放池和Objective-C内存管理的详细信息,请参阅“ 高级内存管理编程指南”。
您可以通过挂起来阻止队列临时执行块对象。您使用该dispatch_suspend
函数挂起调度队列并使用dispatch_resume
函数恢复它。调用dispatch_suspend
会增加队列的暂停引用计数,并调用dispatch_resume
减少引用计数。当引用计数大于零时,队列将保持挂起状态。因此,您必须使用匹配的恢复调用来平衡所有挂起呼叫,以便恢复处理块。
重要说明: 挂起和恢复调用是异步的,仅在执行块之间生效。挂起队列不会导致已执行的块停止。
如果您要提交给调度队列的任务访问某些有限资源,您可能希望使用调度信号量来规范同时访问该资源的任务数。调度信号量的作用类似于常规信号量,但有一个例外。当资源可用时,获取调度信号量所需的时间少于获取传统系统信号量所需的时间。这是因为Grand Central Dispatch没有针对这种特殊情况调用内核。它调用内核的唯一时间是资源不可用时,系统需要停止线程,直到发出信号量信号。
使用调度信号量的语义如下:
创建信号量(使用dispatch_semaphore_create
函数)时,可以指定一个正整数,指示可用资源的数量。
在每个任务中,调用dispatch_semaphore_wait
等待信号量。
当等待呼叫返回时,获取资源并完成您的工作。
完成资源后,释放它并通过调用dispatch_semaphore_signal
函数发信号通知信号量。
有关这些步骤如何工作的示例,请考虑在系统上使用文件描述符。为每个应用程序提供有限数量的文件描述符。如果您有一个处理大量文件的任务,那么您不希望一次打开那么多文件描述符。相反,您可以使用信号量来限制文件处理代码在任何时候使用的文件描述符的数量。您将要包含在任务中的基本代码如下:
// 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_wait
函数会将count变量递减1.如果结果值为负,则函数告诉内核阻塞线程。另一方面,该dispatch_semaphore_signal
函数将count变量递增1以指示资源已被释放。如果有任务被阻止并等待资源,则其中一个随后被解除阻塞并被允许执行其工作。
调度组是一种阻止线程直到一个或多个任务完成执行的方法。您可以在完成所有指定任务之前无法取得进展的位置使用此行为。例如,在分派几个任务来计算某些数据之后,您可以使用一个组来等待这些任务,然后在完成后处理结果。使用调度组的另一种方法是作为线程连接的替代方法。您可以将相应的任务添加到调度组并等待整个组,而不是启动多个子线程然后再连接每个子线程。
清单3-6显示了设置组,向其分派任务以及等待结果的基本过程。不是使用该dispatch_async
函数将任务分派到队列,而是使用该dispatch_group_async
函数。此函数将任务与组关联,并将其排队以执行。要等待一组任务完成,然后使用该dispatch_group_wait
函数,传入适当的组。
清单3-6 等待异步任务
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_sync
从传递给函数调用的同一队列上执行的任务调用该函数。这样做会使队列死锁。如果需要调度到当前队列,请使用该dispatch_async
函数异步执行此操作。
避免从提交给调度队列的任务中获取锁定。虽然使用任务中的锁是安全的,但是当您获得锁时,如果该锁不可用,则可能会完全阻塞串行队列。同样,对于并发队列,等待锁定可能会阻止其他任务执行。如果需要同步部分代码,请使用串行调度队列而不是锁定。
虽然您可以获取有关运行任务的基础线程的信息,但最好避免这样做。有关调度队列与线程兼容性的更多信息,请参阅与POSIX线程的兼容性。
有关如何更改现有线程代码以使用调度队列的其他提示,请参阅迁移远离线程。
原文https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW28