最近的计步器App涉及异步API,我开始试图将CMPedometer的查询方法封装为一个函数,这个函数可以返回 查询方法得到的结果,方便使用。然而事实却没有这么简单,由于查询方法是异步API,因此只要调用该方法 操作就会被分配到另一个线程去做,因此当查询到结果后,调用这个查询方法的方法早已执行完毕,因此返回空, 也就是除了查询方法自带的回调block中可以取到查询结果,其他地方没法获取查询结果。
// stepsInToday永远取不到值
func queryPedometerTodayTotalData() -> Int {
var stepsInToday = 0
self.queryPedometerDataFromDate(NSDate.today()!, toDate: NSDate()) {
(pedometerData, error) in
stepsInToday = (pedometerData?.numberOfSteps.integerValue)!
}
return stepsInToday
}
期间试过给 NSOperation 添加依赖,或者使用 dispatch_group_async,但是用这些方法解决异步API
都不奏效,因为异步API操作始终会分去别的线程处理,
我开始的解决办法是在返回前阻塞等待一段时间(如2秒),这样就解决了时序问题,但是这种方式并不好,
试想如果硬件原因(也可能网络应用相关的延迟)超过这个时间段(2秒),则应用崩溃
// 通过延时执行的解决方法
func queryPedometerTodayTotalData() -> Int {
var stepsInToday = 0
self.queryPedometerDataFromDate(NSDate.today()!, toDate: NSDate()) {
(pedometerData, error) in
stepsInToday = (pedometerData?.numberOfSteps.integerValue)!
}
sleep(2) //用于阻塞线程
return stepsInToday
}
今天突然看到一篇博文,恍然大悟,解决了困惑已久的问题
//通过信号量机制阻塞线程的解决办法
//注:这个方法会阻塞当前线程,因此应该异步调用,后台让后台线程阻塞无妨
func queryPedometerTodayTotalData() -> Int {
var stepsInToday = 0
let semaphore = dispatch_semaphore_create(0)
self.queryPedometerDataFromDate(NSDate.today()!, toDate: NSDate()) {
(pedometerData, error) in
stepsInToday = (pedometerData?.numberOfSteps.integerValue)!
dispatch_semaphore_signal(semaphore)
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
return stepsInToday
}
至此对GCD有了进一步认识,结合这篇博文总结一下:
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(), {
//使用 `result` 做各种事
})
})
dispatch_queue_create 创建自己的队列,需指定队列名和类型(串行还是并发)dispatch_async 而不是 dispatch_sync。dispatch_async 将在block执行前立即返回,而 dispatch_sync 则会等到block执行完毕后才返回。内部的调用可以使用 dispatch_sync(因为不在乎什么时候返回),但是外部的调用必须是 dispatch_async(否则主线程会被阻塞)dispatch_once 这个 API 可以用来创建单例。不过这种方式在 Swift 中已不再重要,Swift 有更简单的方法来创建单例。这里只贴OC的实现:
``` swift
// 在后台队列
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0)
doSomeExpensiveWorkAsynchronously(completionBlock: {
dispatch_semaphore_signal(semaphore)
})
//下面的方法会阻塞线程,直到 dispatch_semaphore_signal 被调用
//这就意味着 signal 必须从不同的线程被调用,因为当前线程已经被阻塞。永远都不应该在主线程中调用 wait,只能在后台线程中调用它。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) //第二个参数是等待超时时间
// 现在开销很大的异步工作已经完成
//既然已经使用了 DISPATCH_QUEUE_SERIAL,那么队列中 work 的执行顺序不应该是先进先出的吗?
//确实是这样,但如果我们把 work 看成是一个耗时的网络操作,其内部是提交到其他线程并发去执行(dispatch_async),也就是每次执行到 work 就立刻返回了,即使最终结果可能还未返回。
//那么我们想要保证队列中的 work 等到前一个 work 执行返回结果后才执行,就需要 semaphore。例子:
typealias DoneBlock = () -> ()
typealias WorkBlock = (DoneBlock) -> ()
class AsyncSerialWorker {
private let serialQueue = dispatch_queue_create("com.qiaolibo.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)
}
}
}
let a = AsyncSerialWorker()
for i in 1...5 {
a.enqueueWork { doneBlock in
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
sleep(arc4random_uniform(4)+1)
print(i)
doneBlock()
}
}
}
//此时的输出结果为:1,2,3,4,5,如果将关于 semaphore 的代码都注释掉,结果就不会是按顺序输出了。
//dispatch_semaphore_create(0) 当两个线程需要协调处理某个事件时,我们在这里传入 0;内部其实是维护了一个计数器,下面会说到。
class LimitedWorker {
private let concurrentQueue = dispatch_queue_create("com.qiaolibo.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)
}
}
}
dispatch_semaphore_wait 都会消耗一次可用数,如果结果为负,函数会告诉内核阻断你的线程。另一方面,dispatch_semaphore_signal 函数每次执行都会将该可用计数 + 1,以此来表明已经释放了资源。如果此刻有因为等待可用资源而被阻隔的任务,系统会从等待的队列中解锁一个任务来执行。NSOperationQueue 的 maxConcurrentOperationCount。如果你使用原生的 GCD 队列而不是 NSOperationQueue,你就能使用信号量来限制并发任务的数量。enqueueWork 都会将 work 提交到一个并发队列,而该并发队列收到任务就会丢出去执行,直到触碰到信号量数量耗尽的天花板(work 入队列的速度太快,dispatch_semaphore_wait 已经消耗完了所有的数量,而之前的 work 还未执行完毕,dispatch_semaphore_signal 不能增加信号量的可用数量)dispatch_group_async 允许你在队列中添加任务(这些任务应该是同步执行的,像计步器的异步查询API放这就无效),而且你会追踪有多少被添加的任务。注意:同一个 dispatch group 能够添加不同队列上的任务,并且能保持对所有组内任务的追踪。当所有被追踪的任务完成时,一个传递给 dispatch_group_notify 的 block 会被触发执行,有点类似于 completion 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(), {
// 所有任务都已完成
}
深入细节:
// 必须在后台队列使用
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)
// 所有任务都已完成
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_group_waitnotify 可以在主线程中调用,而 wait 只能在后台线程中调用(至少 wait 部分要在后台线程中调用,因为它会完全阻塞当前线程)//先构建一个标识映射 Identity Map,一个标识映射是一个字典,表示从 ID 到 model 对象的映射
//标识映射(Identity Map)模式将所有已加载对象放在一个映射中,确保所有对象只被加载一次,并且在引用这些对象时使用该映射来查找对象。
//在处理数据并发访问时,需要一种策略让多个用户共同操作同一个业务实体,这个很重要。
//同样重要的是,单个用户在一个长运行事务或复杂事务中始终使用业务实体的一致版本。
//标识映射模式会为事务中使用所有的业务对象保存一个版本,如果一个实体被请求两次,会得到同一个实体。
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,内存将会崩坏,因为线程会操作相同的引用。
//这也是操作系统中的经典的"读者-写者问题,简而言之,我们可以在同一时刻有多个读者,但同一时刻只能有一个线程可以写入。
dispatch_syncdispatch_asyncdispatch_barrier_syncdispatch_barrier_asyncbarrier 集合 API 提供了解决方案:它们会在队列中的任务清空后执行 block。使用 barrier API 可以限制我们对字典对象的写入,并且确保我们不会在同一时刻执行多个写操作,或者在执行写操作同时执行读操作
class IdentityMap<T: Identifiable> {
var dictionary = Dictionary<String, T>()
let accessQueue = dispatch_queue_create("com.qiaolibo.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 方法就需要一个 completion block)。因为 accessQueue 是并发队列,这些同步读取操作可以并发执行,也就是允许同时读dispatch_barrier_async 将分发 block 到隔离队列上,async 异步部分意味着会立即返回,并不会等待 block 执行完毕。这对性能有好处,但是在一个写操作后立即执行一个读操作会导致读到一个半成品的数据(因为可能写操作还未完成就开始读了)dispatch_barrier_async 中 barrier 部分的逻辑是:barrier block 进入队列后不会立即执行,而是会等待该队列其他 block 执行完毕后再执行。这就保证了我们的 barrier block 每次都只有它自己在执行。而所有在它之后提交的 block 也会一直等待这个 barrier block 执行完再执行dispatch_barrier_async() 函数的 queue,必须是 dispatch_queue_create 创建的并发 queue。如果是串行 queue 或者是 global concurrent queues,这个函数就变成 dispatch_async() 了GCD 是一个具备底层特性的框架,通过它,我们可以构建高层级的抽象行为。这篇文章较为全面的讲述了 GCD 的相关应用领域,对iOS多线程有了更进一步的认识,其中很多内容还只停留在概念层面,待以后实际运用到再深入理解。
Written on May 13th, 2016 by 乔黎博