GCD 使用指南

做者:Soroush Khanlou,原文连接,原文日期:2016-04-25
译者:walkingway;校对:numbbbbb;定稿:numbbbbbhtml

Grand Central Dispatch 大中枢派发? 或俗称 GCD 是一件极其强大的武器。它为你提供了不少底层工具(好比队列和信号量),你能够组合这些工具来实现本身想要的多线程效果。不幸的是,这些基于 C 的 API 晦涩难懂,此外将低级工具组合起来实现高抽象层级 API(译者注:相似于 NSOperation)也不是一件容易的事。在这篇文章中,我会教你们如何利用 GCD 提供的工具来实现高抽象层级的行为。英文原文ios

后台执行

这或许是 GCD 提供的最简单的工具了,你能够在后台线程中处理一些工做,处理完毕后返回主线程继续执行(由于 UIKit 相关的操做只能在主线程中进行)。编程

在本指南中,我将使用 doSomeExpensiveWork() 函数来表示一个长时间执行的任务,它会返回一个值。swift

这种模式的代码以下所示:数组

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_PRIORITY_DEFAULT,咱们几乎用不到其余的优先级选项。dispatch_get_global_queue() 将返回一个队列,其中可能有数百个线程在并发执行。若是你常常须要在后台队列执行开销庞大的操做,那能够用 dispatch_queue_create 建立本身的队列,dispatch_queue_create 带两个参数,第一个是须要指定的队列名,第二个说明是串行队列仍是并发队列。安全

注意,每次调用使用的是 dispatch_async 而不是 dispatch_syncdispatch_async 将在 block 执行前当即返回,而 dispatch_sync 则会等到 block 执行完毕后才返回。内部的调用可使用 dispatch_sync(由于不在意何时返回),可是外部的调用必须是 dispatch_async(不然主线程会被阻塞)。网络

建立单例

dispatch_once 这个 API 能够用来建立单例。不过这种方式在 Swift 中已再也不重要,Swift 有更简单的方法来建立单例。我这里就只贴 OC 的实现:多线程

objectivec
  • (instancetype) sharedInstance {并发

    static dispatch_once_t onceToken;  
       static id sharedInstance;  
       dispatch_once(&onceToken, ^{  
           sharedInstance = [[self alloc] init];  
       });  
       return sharedInstance;

    }app

摊平 completion block

至此咱们的 GCD 之旅开始变得有趣起来。咱们可使用信号量来阻塞一个线程任意时间,直到一个信号从另外一个线程发出。信号量和 GCD 的其余部分同样是线程安全的,而且可以从任意位置被触发。

若是你想同步执行一个异步 API,那你可使用信号量,可是你不能修改它。

objectivec
// 在后台队列
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0)
doSomeExpensiveWorkAsynchronously(completionBlock: {
    dispatch_semaphore_signal(semaphore)
})
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
// 如今开销很大的异步工做已经完成

调用 dispatch_semaphore_wait 会阻塞线程,直到 dispatch_semaphore_signal 被调用。这就意味着 signal 必须从不一样的线程被调用,由于当前线程已经被阻塞。你永远都不该该在主线程中调用 wait,只能在后台线程中调用它。

你能够在调用 dispatch_semaphore_wait 时设置一个超时时间,可是我通常会使用 DISPATCH_TIME_FOREVER

为何在已有 completion 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)
        }
    }
}

上面这个简短的类建立了一个串行队列,容许你将 work 的入队列操做放进 block 中。WorkBlock 须要一个 DoneBlock 做为参数,而 DoneBlock 会在当前工做结束时被执行,咱们经过将 DoneBlock 设置为 {dispatch_semaphore_signal(semaphore)} 来调整信号量,从而让串行队列继续执行下去。

译者注:既然已经使用了 DISPATCH_QUEUE_SERIAL,那么队列中 work 的执行顺序不该该是先进先出的吗?确实是这样,但若是咱们把 work 当作是一个耗时的网络操做,其内部是提交到其余线程并发去执行(dispatch_async),也就是每次执行到 work 就马上返回了,即便最终结果可能还未返回。那么咱们想要保证队列中的 work 等到前一个 work 执行返回结果后才执行,就须要 semaphore。说了这么多仍是举个例子吧,打开 Playground:

import UIKit
import XCPlayground

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)
        }
    }
}

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()
        }
    }
}

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

此时的输出结果为:1,2,3,4,5,若是将关于 semaphore 的代码都注释掉,结果就不会是按顺序输出了。

dispatch_semaphore_create(0) 当两个线程须要协调处理某个事件时,咱们在这里传入 0;内部实际上是维护了一个计数器,下面会说到。

限制并发的数量

在上面的例子中,信号量被用做一个简单的标志,但它也能够当成一个有限资源的计数器。若是你想针对某些特定的资源限制链接数,能够这样作:

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)
        }
    }
}

这个例子来自于苹果官方的多线程编程指南,官方给出的解释以下:

在建立信号量时,能够限定资源的可用数。这个可用数(long 类型)会在信号量初始化时做为参数传入。每次等待信号量时,dispatch_semaphore_wait 都会消耗一次可用数,若是结果为负,函数会告诉内核阻断你的线程。另外一方面,dispatch_semaphore_signal 函数每次执行都会将该可用计数 + 1,以此来代表已经释放了资源。若是此刻有由于等待可用资源而被阻隔的任务,系统会从等待的队列中解锁一个任务来执行。

这个效果相似 NSOperationQueuemaxConcurrentOperationCount。若是你使用原生的 GCD 队列而不是 NSOperationQueue,你就能使用信号量来限制并发任务的数量。

值得注意是:每次调用 enqueueWork 都会将 work 提交到一个并发队列,而该并发队列收到任务就会丢出去执行,直到触碰到信号量数量耗尽的天花板(work 入队列的速度太快,dispatch_semaphore_wait 已经消耗完了全部的数量,而以前的 work 还未执行完毕,dispatch_semaphore_signal 不能增长信号量的可用数量)

等待许多并发任务完成

若是你有许多 blocks 任务要去执行,你须要在它们所有完成时获得通知,那可使用 group。dispatch_group_async 容许你在队列中添加任务(这些任务应该是同步执行的),并且你会追踪有多少被添加的任务。注意:同一个 dispatch group 可以添加不一样队列上的任务,而且能保持对全部组内任务的追踪。当全部被追踪的任务完成时,一个传递给 dispatch_group_notifyblock 会被触发执行,有点相似于 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(), {
    // 全部任务都已完成
}

这个例子很好地展现了如何摊平 completion block。Dispatch group 会在 block 返回时调用 completion block,因此你须要在 block 中等待全部任务完成。

下面这个例子更加详细地展现了 dispatch group 的用法,若是你的任务已是异步,能够这样使用:

// 必须在后台队列使用
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)

// 全部任务都已完成

这段代码更加复杂,不过认真阅读仍是能看懂的。和信号量同样,groups 一样保持着一个线程安全的、能够操控的内部计数器。你可使用这个计数器来确保在 completion block 执行前,多个大开销任务都已执行完毕。使用 enter 来增长计数器,使用 leave 来减小计数器。dispatch_group_async 已为你处理了这些细节,因此尽情地享受便可。

代码片断的最后一行是 wait 调用:它会阻断当前线程而且等计数器到 0 时继续执行。注意,虽然你使用了 enter/leave API,但你仍是可以经过 dispatch_group_notify 将 block 提交到队列中。反过来也成立:若是你用了 dispatch_group_async API,也能使用 dispatch_group_wait

dispatch_group_waitdispatch_semaphore_wait 同样接收一个超时参数。再次重申,我更喜欢 DISPATCH_TIME_FOREVER。另外,不要在主线程中调用 dispatch_group_wait

上面两段代码最大的区别是,notify 能够在主线程中调用,而 wait 只能在后台线程中调用(至少 wait 部分要在后台线程中调用,由于它会彻底阻塞当前线程)。

隔离队列

Swift 中的字典(和数组)都是值类型,当它们被修改时,它们的引用会被一个新的副本所替代。可是,由于更新 Swift 对象的实例变量操做并非原子性的,因此这些操做不是线程安全的。若是两个线程同一时间更新一个字典(好比都添加一个值),并且这两个操做都尝试写同一块内存,这就会致使内存崩坏。咱们可使用隔离队列来实现线程安全。

先构建一个标识映射 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,内存将会崩坏,由于线程会操做相同的引用。这也是操做系统中的经典的读者-写者问题,简而言之,咱们能够在同一时刻有多个读者,但同一时刻只能有一个线程能够写入。

幸运的是 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 方法就须要一个 completion block)。由于 accessQueue 是并发队列,这些同步读取操做能够并发执行,也就是容许同时读。

dispatch_barrier_async 将分发 block 到隔离队列上,async 异步部分意味着会当即返回,并不会等待 block 执行完毕。这对性能有好处,可是在一个写操做后当即执行一个读操做会致使读到一个半成品的数据(由于可能写操做还未完成就开始读了)。

dispatch_barrier_asyncbarrier 部分的逻辑是:barrier block 进入队列后不会当即执行,而是会等待该队列其余 block 执行完毕后再执行。这就保证了咱们的 barrier block 每次都只有它本身在执行。而全部在它以后提交的 block 也会一直等待这个 barrier block 执行完再执行。

传入 dispatch_barrier_async() 函数的 queue,必须是 dispatch_queue_create 建立的并发 queue。若是是串行 queue 或者是 global concurrent queues,这个函数就变成 dispatch_async()

总结

GCD 是一个具有底层特性的框架,经过它,咱们能够构建高层级的抽象行为。若是还有一些我没提到的能够用 GCD 构建的高层行为,请告诉我。

本文由 SwiftGG 翻译组翻译,已经得到做者翻译受权,最新文章请访问 http://swift.gg

相关文章
相关标签/搜索