给iOS开发者的GCD用户手册
Grand Central Dispatch,或者GCD,是一个极其强大的工具。它给你一些底层的组件,像队列和信号量,让你可以通过一些有趣的方式来获得有用的多线程效果。可惜的是,这个基于C的API是一个有点神秘,它不会明显的告诉你如何使用这个底层组件来实现更高层次的方法。在这篇文章中,我希望描述那些你可以通过GCD提供给你的底层组件来实现的一些用法。
后台工作
也许最简单的用法,GCD让你在后台线程上做一些工作,然后回到主线程继续处理,因为像那些属于 UIKit 的组件只能(主要)在主线程中使用。
在本指南中,我将使用 doSomeExpensiveWork() 方法来表示一些长时间运行的有返回值的任务。
这种模式可以像这样建立起来:
let defaultPriority = DISPATCH_QUEUE_PRIORITY_DEFAULT
let backgroundQueue = dispatch_get_global_queue(defaultPriority, )
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;
}
扁平化一个完整的block
现在 GCD 开始变得有趣了。使用一个信号量,我们可以让一个线程暂停任意时间,直到另一个线程向它发送一个信号。这个信号量,就像 GCD 其余部分一样,是线程安全的,并且他们可以从任何地方被触发。
当你需要去同步一个你不能修改的异步API时,你可以使用信号量解决问题。
// on a background queue
dispatch_semaphore_t semaphore = dispatch_semaphore_create()
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()
work({
dispatch_semaphore_signal(semaphore)
})
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}
}
}
这一小类可以创建一个串行队列,并允许你将工作添加到 block 中。当你的工作完成后, WorkBlock 会调用 DoneBlock ,开启信号量,并允许串行队列继续。
限制并发 block 的数量
在前面的例子中,信号量作为一个简单的标志,但它也可以被用来作为一种有限的资源计数器。如果你想在一个特定资源上打开特定数量的连接,你可以使用下面的代码:
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/leaveAPI,你也可以在在队列中添加一个 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的高级用法而我没有罗列在这里,我喜欢听到它们并将它们添加到列表中。
给iOS开发者的GCD用户手册的更多相关文章
- IOS任务管理之GCD使用
前言: 前天学了IOS的NSOperation基本使用,我们得知NSOperation也是基于IOS GCD(Grand Central Dispatch)实现,其实在做IOS开发中GCD已经基本上能 ...
- [iOS]深入理解GCD
看到一篇很好的文章,本来想翻译的,但发现已经有人翻译了,遂简单整理了一下,方便阅读学习 新博客[wossoneri.com] 什么是GCD GCD(Grand Central Dispatch)是li ...
- 一个iOS开发者的修真之路
在微信上有童鞋问我iOS开发者的入门标准是神马?这个问题难到我了,而且贸然给一个答案出来的话,必定会有万千高手来喷. 凡人修仙,仙人修道,道人修真.当我们还是一个在青石板上蹲马步汗水涔涔的废柴时,或许 ...
- 苹果IOS开发者账号的区别,企业账号,个人账号,公司团队账号,教育账号
苹果IOS开发者账号总结 详细地址:https://developer.apple.com/programs/which-program/ 个人账号(Individual): 费用99美金一年, 该账 ...
- 苹果IOS开发者账号总结--发布应用APP时team name是否可以随意写?
个人账号(Individual): 费用99美金一年, 该账号在App Store销售者只能显示个人的ID,比如zhitian zhang,单人使用.个人账号只能有一个开发者.100个苹果的iOS设备 ...
- <转>iOS9 Day-by-Day:iOS开发者必须了解的iOS 9新技术与API
iOS9 Day-by-Day是作者Chris Grant新开的一个系列博客,覆盖了iOS开发者必须知道的关于iOS 9的新技术与API,并且还进行了实际操作演练,每篇文章中相关的代码Chris都会将 ...
- 一个资深iOS开发者对于React Native的看法
一个资深iOS开发者对于React Native的看法 当我第一次尝试ReactNative的时候,我觉得这只是网页开发者涉足原生移动应用领域的歪门邪道. 我认为一个js开发者可以使用javasc ...
- iOS 开发者必知的 75 个工具(译文)
原文地址:http://benscheirman.com/2013/08/the-ios-developers-toolbelt (需FQ) 如果你去到一位熟练的木匠的工作室,你总是能发现他/她有 ...
- iOS开发者必备的10款工具
当前iOS和Android两大移动操作系统“二足鼎立”,几乎覆盖了市面上大部分的智能手机.相比Android,iOS开发适配更简单,且随着各种实用工具和Swift语言的出现,iOS开发门槛地降低,开发 ...
随机推荐
- Linux之Vim编辑器使用
vim文本编辑器用于建立 编辑 显示文本文件,vim没有菜单,只有命令 在windows 平台下可使用gvim进行编写 Vim三种工作模式: 常有命令: 1.INSERT插入命令 i 在光标前插入 I ...
- xml和html之间相互转换
一.xml转换html xml+xslt是典型的数据与表现分离的设计方式.当然,你可以直接转换成HTML,但是如果你要进行整体变化的时候,XML+XSLT的优势就体现出来了.同样的数据,因为你已经有X ...
- python自动开发之第十八天
一.JS正则 test - 判断字符串是否符合规定的正则 rep = /\d+/; rep.test("asdfoiklfasdf89asdfasdf") # true rep = ...
- 原生JS写Ajax的请求函数
一.JS原生ajax ajax:一种请求数据的方式,不需要刷新整个页面:ajax的技术核心是 XMLHttpRequest 对象:ajax 请求过程:创建 XMLHttpRequest 对象.连接服务 ...
- SignalTap II应用小实例之触发位置
概述 SignalTap II一直以来都是笔者调试Altera FPGA设计的利器,最近比较有时间静下心来研究SignalTap II某些细节,虽然笔者有过不少关于SignalTap的使用,且也发表过 ...
- 转:使用 Docker 搭建 Java Web 运行环境
原文来自于:http://www.codeceo.com/article/docker-java-web-runtime.html Docker 是 2014 年最为火爆的技术之一,几乎所有的程序员都 ...
- xml格式化写入文件
参考xml文件地址:http://cloudprint.cainiao.com/template/standard/101 代码: package main import ( "encodi ...
- IE8一枝独秀的JS兼容BUG
// 例如淡入淡出的封装类文件 function ImagesEff(div,time){ this.arr=[];//装载所有div this.time=time; this.recordOld=n ...
- unity3d 幻灯片效果实现
上一篇使用的是静态方式进行的加载,采用的数据结构为 数组 该篇文章则是使用动态加载的方式实现: this.objsOfRouses = Resources.LoadAll("images&q ...
- Struts2的注解功能
我们知道通常情况下,Struts2是通过struts.xml配置的.但是随着系统规模的加大我们需要配置的文件会比较大,虽然我们可以根据不同的系统功能将不同模块的配置文件单独书写,然后通过<inc ...