- 原文连接 : The GCD Handbook
Grand Central Dispatch,或者GCD,是一个极其强大的工具。它给你一些底层的组件,像队列和信号量,让你能够经过一些有趣的方式来得到有用的多线程效果。惋惜的是,这个基于C的API是一个有点神秘,它不会明显的告诉你如何使用这个底层组件来实现更高层次的方法。在这篇文章中,我但愿描述那些你能够经过GCD提供给你的底层组件来实现的一些用法。html
也许最简单的用法,GCD让你在后台线程上作一些工做,而后回到主线程继续处理,由于像那些属于 UIKit
的组件只能(主要)在主线程中使用。ios
在本指南中,我将使用 doSomeExpensiveWork()
方法来表示一些长时间运行的有返回值的任务。git
这种模式能够像这样创建起来:github
let defaultPriority = DISPATCH_QUEUE_PRIORITY_DEFAULT
let backgroundQueue = dispatch_get_global_queue(defaultPriority, 0)
dispatch_async(backgroundQueue, {
let result = doSomeExpensiveWork()
dispatch_async(dispatch_get_main_queue(), {
//use `result` somehow
})
})
复制代码
在实践中,我从不使用任何队列优先级除了 DISPATCH_QUEUE_PRIORITY_DEFAULT
。这返回一个队列,它能够支持数百个线程的执行。若是你的耗性能的工做老是在一个特定的后台队列中发生,你也可用经过 dispatch_queue_create
方法来建立本身的队列。 dispatch_queue_create
能够建立一个任意名称的队列,不管它是串行的仍是并行的。安全
注意每个调用使用 dispatch_async
,不使用 dispatch_sync
。dispatch_async
在 block 执行前返回,而 dispatch_sync
会等到 block 执行完毕才返回。内部的调用可使用 dispatch_sync
(由于无论它何时返回),但外部必须调用 dispatch_async
(不然,主线程会被阻塞)。多线程
dispatch_once
是一个能够被用来建立单例的API。在 Swift 中它再也不是必要的,由于 Swift 中有一个更简单的方法来建立单例。为了之后,固然,我把它写在这里(用 Objective-C )。并发
+ (instancetype) sharedInstance {
static dispatch_once_t onceToken;
static id sharedInstance;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
复制代码
如今 GCD 开始变得有趣了。使用一个信号量,咱们可让一个线程暂停任意时间,直到另外一个线程向它发送一个信号。这个信号量,就像 GCD 其他部分同样,是线程安全的,而且他们能够从任何地方被触发。app
当你须要去同步一个你不能修改的异步API时,你可使用信号量解决问题。框架
// on a background queue
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0)
doSomeExpensiveWorkAsynchronously(completionBlock: {
dispatch_semaphore_signal(semaphore)
})
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
//the expensive asynchronous work is now done
复制代码
dispatch_semaphore_wait
会阻塞线程直到 dispatch_semaphore_signal
被调用。这就意味着 signal
必定要在另一个线程中被调用,由于当前线程被彻底阻塞。此外,你不该该在在主线程中调用 wait
,只能在后台线程。异步
在调用 dispatch_semaphore_wait
时你能够选择任意的超时时间,可是我倾向于一直使用 DISPATCH_TIME_FOREVER
。
这可能不是彻底显而易见的,为何你要把已有的一个完整的 block 代码变为扁平化,但它确实很方便。我最近使用的一种状况是,执行一系列必须连续发生的异步任务。这个使用这种方式的简单抽象被称做 AsyncSerialWorker
:
typealias DoneBlock = () -> ()
typealias WorkBlock = (DoneBlock) -> ()
class AsyncSerialWorker {
private let serialQueue = dispatch_queue_create("com.khanlou.serial.queue", DISPATCH_QUEUE_SERIAL)
func enqueueWork(work: WorkBlock) {
dispatch_async(serialQueue) {
let semaphore = dispatch_semaphore_create(0)
work({
dispatch_semaphore_signal(semaphore)
})
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}
}
}
复制代码
这一小类能够建立一个串行队列,并容许你将工做添加到 block 中。当你的工做完成后, WorkBlock
会调用 DoneBlock
,开启信号量,并容许串行队列继续。
在前面的例子中,信号量做为一个简单的标志,但它也能够被用来做为一种有限的资源计数器。若是你想在一个特定资源上打开特定数量的链接,你可使用下面的代码:
class LimitedWorker {
private let concurrentQueue = dispatch_queue_create("com.khanlou.concurrent.queue", DISPATCH_QUEUE_CONCURRENT)
private let semaphore: dispatch_semaphore_t
init(limit: Int) {
semaphore = dispatch_semaphore_create(limit)
}
func enqueueWork(work: () -> ()) {
dispatch_async(concurrentQueue) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
work()
dispatch_semaphore_signal(semaphore)
}
}
}
复制代码
这个例子从苹果的Concurrency Programming Guide拿来的。他们能够更好的解释在这里发生了什么:
当你建立一个信号量时,你能够指定你的可用资源的数量。这个值是信号量的初始计数变量。你每一次等待信号量发送信号时,这个
dispatch_semaphore_wait
方法使计数变量递减1。若是产生的值是负的,则函数告诉内核来阻止你的线程。在另外一端,这个dispatch_semaphore_signal
函数递增count变量用1表示资源已被释放。若是有任务阻塞和等待资源,其中一个随即被放行并进行它的工做。
其效果相似于 maxConcurrentOperationCount
在 NSOperationQueue
。若是你使用原 GCD队 列而不是 NSOperationQueue
,你可使用信号庄主来限制同时执行的 block 数量。
一个值得注意的就是,每次你调用 enqueueWork
,若是你打开信号量的限制,就会启动一个新线程。若是你有一个低限而且大量工做的队列,您能够建立数百个线程。一如既往,先配置文件,而后更改代码。
若是你有多 block 工做来执行,而且在他们集体完成时你须要发一个通知,你可使用 group 。dispatch_group_async
容许你在队列中添加工做(在 block 里面的工做应该是同步的),而且记录添加了多少了项目。注意,在同一个 dispatch group 中能够将工做添加到不一样的队列中,而且能够跟踪它们。当全部跟踪的工做完成,这个 block 开始运行 dispatch_group_notify
,就像是一个完整的 block 。
dispatch_group_t group = dispatch_group_create()
for item in someArray {
dispatch_group_async(group, backgroundQueue, {
performExpensiveWork(item: item)
})
}
dispatch_group_notify(group, dispatch_get_main_queue(), {
// all the work is complete
}
复制代码
拥有一个完整的block,对于扁平化一个功能来讲是一个很好的案例。 dispatch group 认为,当它返回时,这个 block 应该完成了,因此你须要这个 block 等待直到其余工做已经完成。
有更多的手动方式来使用 dispatch groups ,特别是若是你耗性能的工做已是异步的:
// must be on a background thread
dispatch_group_t group = dispatch_group_create()
for item in someArray {
dispatch_group_enter(group)
performExpensiveAsyncWork(item: item, completionBlock: {
dispatch_group_leave(group)
})
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
// all the work is complete
复制代码
这段代码是比较复杂的,但经过一行一行的阅读能够帮助理解它。就像信号量,groups 也还保持线程安全,是一个你能够操做的内部计数器。您可使用此计数器来确保在执行完成 block 以前,多个长的运行任务都已完成。使用 “enter” 递增计数器,并用 “leave” 递减计数器。 dispatch_group_async
为你处理全部的这些细节,因此我愿意尽量的使用它。
在这段代码的最后一点是 wait
方法:它会阻塞线程,并等待计数器为0后,继续执行。注意,即便你使用了enter
/leave
API,你也能够在在队列中添加一个 dispatch_group_notify
block.反过来也是对的:当你使用 dispatch_group_async
API时你也可使用 dispatch_group_wait
。
dispatch_group_wait
,就像dispatch_semaphore_wait
同样,能够设置超时。再一次声明,DISPATCH_TIME_FOREVER
已很是足够使用, 我从未以为须要使用其余的来设置超时。固然就像 dispatch_semaphore_wait
同样,永远不要在主线程使用 dispatch_group_wait
。
二者之间最大的区别是,使用 notify
能够彻底从主线程调用,而使用 wait
,必须发生在后台队列(至少 wait
的部分,由于它会彻底阻塞当前队列)。
Swift 语言的 Dictionary
(和 Array
)类型都是值类型。 当他们被改变时, 他们的引用会彻底被新的结构给替代。固然,由于更新实例变量的 Swift 对象不是原子性的,它们不是线程安全的。双线程能够在同一时间更新一个字典(例如,增长一个值),而且两个尝试写在同一块内存,这可能致使内存损坏。咱们可使用隔离队列来实现线程安全。 让咱们建立一个identity map。 identity map 是一个字典,将项目从其ID
属性映射到模型对象。
class IdentityMap<T: Identifiable> {
var dictionary = Dictionary<String, T>()
func object(forID ID: String) -> T? {
return dictionary[ID] as T?
}
func addObject(object: T) {
dictionary[object.ID] = object
}
}
复制代码
这个对象基本上是一个字典的包装器。若是咱们的方法 addObject
同一时间被多个线程所调用,它可能会损害内存,由于这些线程对对同一个引用进行处理。这被称之为 readers-writers problem。总之,咱们能够同时有多个读者阅读,可是只有一个线程能够在任何给定的时间写。 幸运的是,GCD 给了咱们很好的工具去处理这样的状况。咱们可使用如下四种 API :
dispatch_sync
dispatch_async
dispatch_barrier_sync
dispatch_barrier_async
咱们理想的状况是,读同步,同时,而写能够异步,当引用该对象时必须是惟一的。 GCD 的 barrier
API集能够作一些特别的事情:他们执行 block 以前必须等到队列彻底空了。使用 barrier
API去进行字典写入的操做将会被限制,这样确保咱们永远不会有任何写入发生在同一时间,不管是读取或是写入。
class IdentityMap<T: Identifiable> {
var dictionary = Dictionary<String, T>()
let accessQueue = dispatch_queue_create("com.khanlou.isolation.queue", DISPATCH_QUEUE_CONCURRENT)
func object(withID ID: String) -> T? {
var result: T? = nil
dispatch_sync(accessQueue, {
result = dictionary[ID] as T?
})
return result
}
func addObject(object: T) {
dispatch_barrier_async(accessQueue, {
dictionary[object.ID] = object
})
}
}
复制代码
dispatch_sync
将 block 添加到咱们的隔离队列,而后等待它在返回以前执行。这样,咱们就会有咱们的同步阅读的结果。(若是咱们没有作到同步,咱们的 getter 方法可能须要一个完成的 block 。)由于 accessQueue
是并发的,这些同步读取就能同时发生。 dispatch_barrier_async
将 block 添加到隔离队列。这个 async
部分意味着它将实际执行的 block 以前返回(执行写入操做)。这对咱们的表现有好处,但也有一个缺点是,在 “write” 操做后当即执行 “read” 操做可能会致使获取改变以前的旧数据。 这个 dispatch_barrier_async
的 barrier
部分,意味着它将等待直到当前运行队列中的每一个 block 执行完毕后才执行。其余 block 将在它后面排队,当barrier调度完成时执行。
Grand Central Dispatch 是一个有不少底层语言的框架。使用它们,这个是我能创建的比较高级的技术。若是有其余一些你使用的GCD的高级用法而我没有罗列在这里,我喜欢听到它们并将它们添加到列表中。