Grand Central Dispatch(GCD)
调度队列(dispatch queues
)是执行任务的强大工具,它使你可以相对于调用者异步或同步地执行任意代码块。适用于 ios 4.0+
。
与相应的线程代码相比,dispatch queues
的优点是,使用起来更简单,能更有效地执行任务。
Dispatch Queues
Dispatch queue
是在应用程序中异步并发执行任务的一种简便方法。task
(任务) 只是应用程序需要执行的一些工作,如:可以定义一个任务来执行一些计算、创建或修改数据结构、处理从文件中读取的一些数据,或者执行任意数量的操作。通过将相应的代码放置在函数或block
对象中并将其添加到dispatch queue
来定义任务。dispatch queue
是一种类似对象的结构,用于管理提交给它的任务。所有dispatch queue
都是first-in, first-out
(先进先出)数据结构。因此,添加到队列中的任务总是按照添加时相同的顺序启动。GCD
会自动为你提供一些dispatch queues
,但也可以为特定目的创建其他dispatch queues
。
Dispatch Queues 的类型
串行队列
Serial queues
(串行队列,又称 private dispatch queues
) 按添加到队列的顺序一次执行一个任务。当前执行的任务运行在 dispatch queue
管理的不同线程上(可能因任务而异)。
Serial queues
通常用于同步对特定资源的访问。可以根据需要创建任意多个Serial queues
,并且每个队列都与所有其他队列并行运行。
并发队列
Concurrent queues
(并发队列,也称为 global dispatch queue
的一种) 可以并发执行一个或多个任务,但是任务仍然按照将它们添加到队列中的顺序启动。当前执行的任务在 dispatch queue
管理的不同线程上运行。在任何给定点执行的任务的确切数量是可变的,这取决于系统条件。
在iOS 5
及更高版本中,您可以通过将 DISPATCH_QUEUE_CONCURRENT
指定为队列类型来自己创建 concurrent dispatch queues
。另外,有四个预定义的全局并发队列供你的应用程序使用。
主调度队列
main dispatch queue
是全局可用的serial queue
,它在应用程序的主线程上执行任务。
main dispatch queue
与 应用程序的run loop
(如果存在)一起工作,将队列任务的执行 与 附加到run loop
的其他event sources
的执行 交织在一起。
因为main dispatch queue
在应用程序的主线程上运行,所以它通常用作应用程序的关键同步点。尽管你无需创建main dispatch queue
,但仍需要确保您的应用程序适当地消耗它。
dispatch queues
的优点
在将并发性添加到应用程序时,dispatch queues
比线程有几个优势。
1、最直接的好处是work-queue programming model
(工作队列编程模型)的简单性。
使用线程,你必须为要执行的工作以及线程本身的创建和管理编写代码。Dispatch queues
使你可以专注于实际要执行的工作,而无需担心线程的创建和管理。相反,系统将为你处理所有线程的创建和管理。优点是系统能够比任何单个应用程序更有效地管理线程。系统可以根据 可用资源 和 当前系统状况 动态缩放线程数。此外,系统通常能够比你自己创建线程更快地开始运行任务。
2、dispatch queues
编写代码通常比为线程编写代码容易
虽然你可能认为为dispatch queues
改写代码很困难,但是为dispatch queues
编写代码通常比为线程编写代码容易。编写代码的关键是 设计独立 且 能够异步运行的任务(实际上,对于线程和dispatch queues
都是如此)。
3、dispatch queues
的优势在于可预测性
如果有两个任务访问相同的共享资源,但在不同的线程上运行,则每个线程都可以首先修改资源,并且需要使用锁来确保这两个任务不会同时修改该资源。使用dispatch queues
,你可以将两个任务都添加到serial dispatch queue
中,以确保在任何给定时间只有一个任务修改了资源。这种基于queue-based synchronization
(队列的同步)比locks
更有效,因为在有争用还是无争用的情况下,locks
总是需要昂贵的kernel trap
(内核陷阱),而dispatch queues
主要在应用程序的进程空间中工作,并且只有在绝对必要时才调用内核。
虽然你指出在 serial queue
中运行的两个任务不能同时运行是正确的,但你必须记住,如果两个线程同时获得一个lock
,则线程提供的任何并发性都会丢失或显着减少。更重要的是,线程模型需要创建两个线程,这两个线程同时占用内核和用户空间内存。Dispatch queues
不会为它们的线程支付相同的内存损失,它们使用的线程会一直保持忙碌,不会阻塞。
4、dispatch queues
的一些要点:
dispatch queues
相对于其他dispatch queues
并行执行任务。任务的序列化仅限于单个dispatch queues
中的任务。系统决定任何一次执行的任务总数。因此,在100个不同的
queues
中具有100个任务的应用程序可能不会并发执行所有这些任务(除非它具有100个或更多个有效内核)。在选择启动哪个新任务时,系统会考虑
queue priority levels
(队列优先级)。可以设置serial queue
优先级。queue
中的任务在添加到queue
时必须准备就绪才能执行。(如果以前使用过Cocoa operation objects
,请注意,此行为与model operations
使用的行为有所不同。)private dispatch queues
(即,串行队列) 是引用计数的对象(reference-counted objects
)。请注意,除了将queue
保留在自己的代码中之外,还可以将dispatch sources
附加到queue
并增加其retain count
。因此,你必须确保取消所有dispatch sources
,并且所有retain
调用 都与适当的release
调用平衡。
队列相关技术
除了dispatch queues
,Grand Central Dispatch
还提供了几种使用 queues
的技术来帮助您管理代码。
Dispatch groups
dispatch groups
(调度组) 是一种监视一组 block
对象是否完成的方法,你可以根据需要同步或异步监视这些block
。Groups
为依赖于其他任务完成的代码 提供了一种有用的同步机制。
Dispatch semaphores
Dispatch semaphores
(调度信号量)与传统信号量相似,但通常效率更高。只有在由于semaphore
不可用 而需要阻塞调用线程时,才将Dispatch semaphores
向下调用到内核。如果semaphore
可用,则不进行内核调用。
Dispatch sources
Dispatch sources
(调度源) 响应于特定类型的系统事件而生成通知。可以使用 dispatch sources
来监视事件,例如进程通知、信号和描述符事件。发生事件时,dispatch source
将任务代码异步提交到指定的dispatch queue
进行处理。
使用block执行任务
block
对象是一种基于C的语言功能,可以在C
、Objective-C
和C++
代码中使用。block
使定义一个独立的工作单元变得容易。尽管它们看起来类似于函数指针,但block
实际上是由类似于对象的底层数据结构来表示的,并且由编译器为你创建和管理。编译器将你提供的代码(以及任何相关数据)打包起来,并将其封装为可以存在于堆中并在应用程序中传递的一种形式。
block
的主要优点之一是它们能够使用其词法范围(lexical scope
)之外的变量。 当你在函数或方法中定义一个block
时,该block
在某些方面就像传统的代码块一样。 例如,一个block
可以读取在父作用域中定义的变量的值。block
访问的变量被复制到堆上的block
数据结构中,以便该block
以后可以访问它们。将block
添加到dispatch queue
时,通常必须以只读格式保留这些值。但是,同步执行的block
也可以使用带有__block
关键字的变量,以将数据返回到父级的调用范围。
你可以使用类似于函数指针的语法来声明与代码内联的块(blocks inline
)。block
和function pointer
之间的主要区别在于,block
名称的前面带有^
符号而不是*
符号。像function pointer
一样,你可以将参数传递给block
并从中接收返回值。
1 | int x = 123; |
以下是设计blocks
时应考虑的一些关键准则的总结:
对于计划使用
dispatch queue
异步执行的blocks
,从父函数或方法中捕获标量变量并在block
中使用它们是安全的。但是,你不应该试图捕获大型结构 或 其他由调用上下文分配和删除的基于指针的变量。在执行block
时,该指针引用的内存可能会消失。当然,自己分配内存(或对象)并显式地将该内存的所有权交给block
是安全的。dispatch queues
复制添加到它们的blocks
,并在完成执行时释放blocks
。换句话说,在将blocks
添加到queue
之前,无需显式复制块。尽管在执行小任务时,
queues
比原始线程更有效,但是创建blocks
并在queues
上执行它们仍然有开销。如果一个blocks
做的工作太少,则内联执行它可能比将其分派到队列中更便宜。判断某个blocks
是否工作量太少的方法是使用性能工具收集每个路径的指标并进行比较。不要缓存与底层线程相关的数据,并期望可以从不同的
block
访问这些数据。如果同一队列中的任务需要共享数据,请改用dispatch queue
的上下文指针存储数据。如果你的
block
创建了多个Objective-C
对象,你可能希望将block
部分代码的代码包含在@autorelease block
中,以处理这些对象的内存管理。 尽管GCD dispatch queues
具有自己的autorelease pools
,但是它们不保证这些pools
何时耗尽。如果您的应用程序受内存限制,则创建自己的autorelease pool
可以让你在更定期地为自动释放对象释放内存。
创建和管理 Dispatch Queues
在将任务添加到queue
之前,必须确定要使用的队列类型以及打算如何使用它。Dispatch queues
可以串行或并行执行任务。此外,如果你对队列有特定的用途,则可以相应地配置queue
属性。
获取 Global Concurrent Dispatch Queues
当你有多个可以并行运行的任务时,concurrent dispatch queue
很有用。concurrent queue
仍然是一个 queue
,因为它按照first-in, first-out
(先进先出)的顺序将任务从队列中取出。但是,concurrent queue
可能会在其他先前任务完成之前使其他任务出队。concurrent queue
在任何给定时刻执行的实际任务数是可变的,可以随着应用程序条件的变化而动态变化。许多因素会影响concurrent queue
执行的任务数量,包括可用内核数、其他进程正在完成的工作量以及其他serial dispatch queues
中任务的数量和优先级。
系统为每个应用程序提供4个concurrent dispatch queues
。这些queues
对于应用程序来说是全局的,仅按优先级进行区分。因为它们是全局的,所以无需显式创建它们,只需要使用 dispatch_get_global_queue
函数请求其中一个队列,如以下示例所示:
1 | /* 通过传递常量获取 queue |
尽管dispatch queues
是reference-counted objects
(引用计数的对象),但是你无需retain
和release
global concurrent queues
,因为它们在应用程序中是全局的,所以忽略对这些queues
调用retain
和release
。因此,不需要存储对这些queues
的引用,只要需要对其中一个队列的引用,就可以调用dispatch_get_global_queue
函数。
创建 Serial Dispatch Queues
当你希望任务以特定顺序执行时,Serial queues
很有用。serial queue
一次只能执行一个任务,并且总是从队列的开头提取任务。可以使用serial queue
而不是lock
来保护共享资源或可变数据结构。与lock
不同,serial queue
可确保任务以可预测的顺序执行。而且,只要你将任务异步提交到serial queue
,该queue
就永远不会死锁(deadlock
)。
与创建的concurrent queues
不同,你必须显式创建和管理要使用的任何serial queue
。可以为应用程序创建任意数量的serial queues
,但是应该避免仅仅为了同时执行尽可能多的任务而创建大量的serial queues
。如果要同时执行大量任务,请将它们提交到一个global concurrent queues
中。创建serial queues
时,请尝试确定每个queue
的用途,例如保护资源或同步应用程序的某些关键行为。
创建自定义串行队列
1 | /* |
在运行时获取 Common Queues
除了你创建的任何自定义队列之外,系统还会自动创建一个serial queue
并将其绑定到应用程序的主线程。
GCD
提供了一些函数,使你可以从应用程序中访问几个常见的dispatch queues
:
使用
dispatch_get_current_queue
函数进行调试或测试当前队列的身份。从block
对象内部调用此函数将返回已提交该block
的队列,并且现在可能正在该队列上运行。从block
外部调用此函数将为您的应用程序返回default concurrent queue
。使用
dispatch_get_main_queue
函数获取与应用程序的主线程关联的serial dispatch queue
。此队列是为Cocoa
应用程序以及调用了dispatch_main
函数或在主线程上配置run loop
的应用程序 自动创建的。使用
dispatch_get_global_queue
函数获取任何共享的concurrent queues
。
Dispatch Queues的内存管理
Dispatch queues
和其它dispatch objects
是reference-counted data types
(引用计数的数据类型)。创建serial dispatch queue
时,它的初始引用计数为1。可以根据需要使用dispatch_retain
和dispatch_release
函数来递增和递减该引用计数。当 queue
的引用计数达到零时,系统将异步释放该queue
。
retain
和release
dispatch objects
很重要,以确保它们在使用时仍保留在内存中。与内存管理的Cocoa
对象一样,一般规则是,如果你打算使用传递给代码的queue
,则应在使用该queue
之前保留该queue
,并在不再需要它时release
它。这种基本模式可确保queue
在你使用期间一直保留在内存中。
注意:你不需要保留或释放任何
global dispatch queues
,包括concurrent dispatch queues
或main dispatch queue
。retain
或release``queues
的任何尝试都将被忽略。
使用 Queue 存储自定义上下文信息
所有dispatch objects
(包括dispatch queues
)都允许你将自定义上下文数据与该对象相关联。要在给定对象上设置和获取此数据,请使用dispatch_set_context
和dispatch_get_context
函数。系统不会以任何方式使用你的自定义数据,你可以在适当的时间分配和取消分配数据。
对于queues
,可以使用上下文数据存储指向Objective-C
对象或其他数据结构的指针,该指针或数据结构有助于识别queues
或其在代码中的预期用途。可以使用queue
的finalizer
函数在释放上下文数据之前将其从queue
中释放(或取消关联)。
为Queue提供清理功能
创建serial dispatch queue
后,可以附加finalizer
函数,以在释放queue
时执行任何自定义清理。Dispatch queues
是引用计数对象,可以使用dispatch_set_finalizer_f
函数指定当queue
的引用计数达到零时要执行的函数。可以使用此函数来清理与queue
关联的上下文数据,并且仅当上下文指针不为NULL
时才调用此函数。
向Queue添加任务
要执行任务,必须将其分派到适当的dispatch queue
。可以同步或异步分配任务,也可以单个或成组分配任务。一旦进入队列,该队列就会根据其约束条件和队列中已经存在的任务,负责尽快执行任务。
向 Queue 添加单个任务
向queue
添加任务有两种方法:asynchronously
(异步)或synchronously
(同步)。
如果可能,使用dispatch_async
和dispatch_async_f
函数进行异步执行优于同步替代方案。当你向queue
添加block
对象或函数时,无法知道该代码何时执行。因此,异步添加block
或函数使你可以安排代码的执行,并继续从调用线程执行其他工作。如果你正在从应用程序的主线程调度任务,这一点尤其重要,这可能是为了响应某些用户事件。
尽管你应该尽可能异步地添加任务,但是有时仍然需要同步添加任务以防止出现竞争条件或其他同步错误。在这些情况下,可以使用dispatch_sync
和dispatch_sync_f
函数将任务添加到queue
中。这些函数将阻塞当前执行线程,直到指定任务完成执行为止。
向并发队列添加任务
1 | dispatch_queue_t queueC = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
向串行队列添加任务
1 | dispatch_queue_t queueS = dispatch_queue_create("com.test.MyCustomQueue", NULL); |
向主队列添加任务
- 只能以异步的方式向主队列添加任务
1 | dispatch_async(dispatch_get_main_queue(), ^{ |
决不能从计划传递给函数的同一
queue
中执行的任务调用dispatch_sync
或dispatch_sync_f
函数。这对于serial queues
特别重要,serial queues
必然会发生死锁,但对于concurrent queues
也应该避免。
造成死锁的情况如下:
1 | // 在主线程执行如下函数会造成死锁崩溃 |
1 | dispatch_queue_t queueS = dispatch_queue_create("com.test.myQueue1", NULL); |
完成任务时执行 Completion Block
从本质上讲,分派到queue
的任务独立于创建它们的代码运行。但是,当任务完成时,应用程序可能仍然希望得到通知,以便它能够合并结果。在传统的异步编程中,你可以使用回调机制来执行此操作,但是在dispatch queues
中,可以使用completion block
。
completion block
只是您在原始任务结束时分派到queue
中的另一段代码。调用代码通常在启动任务时将completion block
作为参数提供。所有任务代码所要做的就是在完成工作时将指定的block
或函数提交给指定的queue
。
1 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
同时执行循环迭代
concurrent dispatch queues
可能提高性能的一个地方是,你有一个执行固定次数迭代的循环。如下for循环,该循环每次迭代中都完成了一些工作:
1 | for (int i = 0; i < 100; i++) { |
如果在每次迭过程中执行的工作与在所有其他迭代过程中执行的工作不同,并且每个连续循环完成的顺序不重要,则可以用调用dispatch_apply
或dispatch_apply_f
函数来替换循环。这些函数在每次循环迭代一次将指定的block
或函数提交给queue
。 因此,当分派到concurrent queue
时,可以同时执行多个循环迭代。
在调用dispatch_apply
或dispatch_apply_f
时,可以指定serial queue
或concurrent queue
。传递concurrent queue
使你可以同时执行多个循环迭代,这是使用这些函数的最常用方法。尽管使用serial queue
是允许的,并且对代码做了正确的事情,但是使用这样的queue
与保留循环相比没有真正的性能优势。
与常规的for循环一样,
dispatch_apply
和dispatch_apply_f
函数只有在完成所有循环迭代之后才返回。因此,当从已经在队列上下文中执行的代码中调用它们时,应该小心。如果作为参数传递给函数的队列是一个serial queue
,并且与执行当前代码的队列相同,则调用这些函数将使queue
死锁。因为它们有效地阻塞了当前线程,所以从主线程调用这些函数时也应小心,因为它们可能阻止事件处理循环及时响应事件。如果循环代码需要大量的处理时间,则可能需要从其他线程调用这些函数。
1 | dispatch_group_t downloadGroup = dispatch_group_create(); |
应该确保你的任务代码在每次迭代中完成了合理的工作量。与分派给queue
的任何block
或函数一样,调度代码执行也会有开销。如果循环的每次迭代仅执行少量工作,则调度代码的开销 可能会超过 将代码调度到queue
可能带来的性能优势。如果在测试期间发现这是正确的,则可以使用striding
(跨步)来增加每次循环迭代期间执行的工作量。 使用striding
,可以将原始循环的多个迭代组合到一个block
中,并按比例减少迭代计数。例如,如果最初执行100次迭代,但决定使用4的步幅,则现在从每个块执行4次循环迭代,你的迭代计数为25。
1 | dispatch_group_t downloadGroup = dispatch_group_create(); |
在主线程上执行任务
Grand Central Dispatch
提供了一个特殊的dispatch queue
,用于在应用程序的主线程上执行任务。所有应用程序均会自动提供此queue
,并由在其主线程上设置run loop
的任何应用程序自动耗尽。如果不创建Cocoa
应用程序,并且不想显式设置run loop
,则必须调用dispatch_main
函数显式地耗尽main dispatch queue
。你仍然可以将任务添加到队列中,但是如果您不调用此函数,则这些任务将永远不会执行。
可以通过调用dispatch_get_main_queue
函数来获取应用程序主线程的dispatch queue
。添加到此queue
的任务在主线程上串行执行。因此,可以将此队列用作在应用程序其他部分中完成工作的同步点。
在任务中使用Objective-C对象
GCD
为Cocoa
内存管理技术提供了内置支持,因此可以在提交给dispatch queues
的blocks
中自由使用Objective-C
对象。每个dispatch queue
都维护自己的autorelease pool
,以确保自动释放的对象在某个时刻被释放。queues
并不保证它们何时真正释放这些对象。
如果您的应用程序受内存限制,而block
创建了多个自动释放的对象,则创建自己的autorelease pool
是确保及时释放对象的唯一方法。如果block
创建了数百个对象,则可能要创建多个autorelease pool
或定期清空你的pool
。
挂起和恢复队列
可以通过挂起一个queue
来临时阻止它执行block
对象。可以使用dispatch_suspend
函数挂起dispatch queue
,然后使用dispatch_resume
函数将其恢复。调用dispatch_suspend
会使队列的挂起引用计数增加,而调用dispatch_resume
会使引用计数减少。当引用计数大于零时,队列保持挂起状态。因此,你必须平衡所有suspend
的调用与匹配的resume
调用,以恢复处理block
。
Suspend
(暂停)和resume
(恢复)调用是异步的,仅在执行block
之间生效。挂起一个队列不会导致已经执行的block
停止。
使用 Dispatch Semaphores 调节有限资源的使用
如果要提交给dispatch queues
的任务访问某个有限资源,则可能要使用dispatch semaphore
(调度信号量)来调节同时访问该资源的任务数。dispatch semaphore
的工作方式类似于常规信号量,但有一个例外。当资源可用时,获取dispatch semaphore
的时间比获取传统system semaphore
的时间要短。这是因为在这种特殊情况,Grand Central Dispatch
不会调用内核,它唯一调用内核的时间是当资源不可用时,系统需要停止线程,直到信号量发出信号。
使用调度信号量的语义如下:
创建
semaphore
时(使用dispatch_semaphore_create
函数),可以指定一个正整数,表示可用资源的数量。在每个任务中,调用
dispatch_semaphore_wait
等待semaphore
。等待调用返回时,获取资源并进行工作。
处理完资源后,请释放该资源,并通过调用
dispatch_semaphore_signal
函数来发出semaphore
。
1 | // 可用资源浴室为2间 |
创建信号量时,请指定可用资源的数量。该值成为semaphore
(信号量)的初始计数变量。每次等待semaphore
(信号量)时,dispatch_semaphore_wait
函数都会将对变量的计数减1。如果结果值为负,则该函数会通知内核阻止您的线程。 另一方面,dispatch_semaphore_signal
函数将count
变量增加1,以指示资源已释放。如果有任务被阻塞并等待资源,则其中一个任务随后将被解除阻塞并允许执行其工作。
等待排队的任务组
Dispatch groups
(调度组)是一种阻塞线程,直到一个或多个任务完成执行的一种方法。可以在所有指定任务完成之前无法取得进展的地方使用它。
使用方式一:在分派了多个任务以计算一些数据之后,使用一个组来等待这些任务,然后在完成后处理结果;
1 | dispatch_group_t group = dispatch_group_create(); |
使用方式二:使用dispatch groups
的另一种方法是替代线程连接。可以将相应的任务添加到dispatch group
并等待整个组,而不是启动几个子线程,然后再与每个子线程联接。
1 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
Dispatch Queues和线程安全
在 dispatch queues
的上下文中谈论线程安全性似乎很奇怪,但是线程安全性仍然是一个相关主题。每当你在应用程序中实现并发时,你都应该了解以下几点:
Dispatch queues
本身是线程安全的。换句话说,可以从系统上的任何线程将任务提交到dispatch queue
,而无需先对该queue
进行 锁定 或 同步对该queue
访问。不要从一个正在执行的任务中调用
dispatch_sync
函数,这个任务和你传递给你的函数调用的是同一个队列。这样做将导致队列死锁。如果需要dispatch
到当前queue
,请使用dispatch_async
函数异步进行。避免从提交到
dispatch queue
的任务中获取locks
。虽然从任务中使用lock
是安全的,但是当你获取lock
时,如果该lock
不可用,则可能会完全阻塞serial queue
。同样,对于concurrent queues
,等待lock
可能会阻止其他任务执行。如果需要同步部分代码,请使用serial dispatch queue
而不是lock
。尽管您可以获得有关运行任务的基础线程的信息,但最好避免这样做。