你可能不知道的使用GCD注意事项

2,184 阅读6分钟

1. 是否应该将任务分发到全局队列 DispatchQueue.global()

“ dispatch_get_global_queue()实际上是调度API提供的最糟糕的东西 ”

参见一个例子 里面很好说明了问题

答案是否定的,事实上分配到 global queue 全局队列对于性能来说是很糟糕的。

当你dispatch_async一个块到并发队列时,GCD将尝试在其线程池中找到一个空闲线程来运行该块。 如果找不到空闲线程,则必须为工作项创建一个新线程。将块快速分配到并发队列可能导致快速创建新线程。

Since the global queue lives right above the thread pool, it has to take liberties with regards to how it handles quality of service and priorities. This means that GCD (or more specifically, its scheduler) has less to work with when scheduling your work.

由于全局队列正好位于线程池之上,因此它必须自由地处理服务质量和优先级。这意味着GCD(或者更具体地说,它的调度程序)在调度您的工作时可以使用的较少。

不要使用DispatchQueue.global()。 全局队列很容易导致线程爆炸: 在sleep / waits / locks 阻塞的线程被libdispatch 认为是不活动的,这反过来会导致在你的程序的其他部分调度时产生新的线程。请注意,无法保证您的线程永远不会阻塞,因为仅仅使用系统库就会导致阻塞。全局队列也不能很好地处理 qos/priorities。苹果的libdispatch维护者宣称它是“分派API提供的最糟糕的东西”。在一个自定义队列(而不是在定义良好的执行上下文之一)上运行代码。

2. 我可以根据需要创建任意多个队列吗?

当GCD于多年前首次发布时,我们被告知不必担心线程而只创建队列。事实证明是错的,GCD并非足够聪明到可以弄清楚如何安排工作并使其高效.

苹果已经改变了这一点,并且在WWDC的过去几年中,他们一直在倡导使用少量的底层队列,您可以在这些底层队列上构建自己的队列层次结构。

并有充分的理由:无限数量的队列会在系统中造成各种问题。由于这些队列中的每一个都有自己的线程,因此可能导致不必要的上下文切换,或者更糟糕的是,导致线程爆炸。

3. 并发队列上比在串行队列上能处理更多的工作,对吗?

不对

Concurrent queues are not as optimized as serial queues. Use them if you measure a performance improvement, otherwise it's likely premature optimization.

并发队列的优化程度不如串行队列。如果要衡量性能改进,请使用它们,否则可能是过早的优化。

attributes: .concurrent 创建的并发调度队列是GCD世界中的二等公民。

串行分派队列可以确保进行各种优化。如果所有工作都在同一线程上执行,则CPU可以使用该线程的执行历史记录来优化您的工作。

此外,上下文切换和资源争用可能会非常昂贵,尤其是在基础工作默认为串行的情况下。

例如,您可以将所有所需的网络请求并行化,但是所有线程将争夺底层硬件接口(即管理实际物理芯片的系统)。通过在串行队列中运行所有这些任务,减少线程之间的争用数量以及减少上下文切换的数量,将为您提供更好的服务。

4. 使用DispatchGroups的坑

这是我在项目中改进了代码,遇到了一个dispatch_group的Crash

<OS_dispatch_group: group[0x28656f390] = { xref = 8, ref = 1, count = 1073741823, gen = 1, waiters = 0, notifs = 0 }>

通过照着这篇文章修改,确实解决了Crash的问题

大原则就是,如果有快速运行的代码,应该提前写好 group.enter(),示例如下:

修改前:

let group = DispatchGroup()

// Hitting something fast, like UserDefaults
DispatchQueue.main.async {
    // 注意这一对
    group.enter()
    group.leave()
    print("leaving first group almost immediately")
}

// Hitting something slow, like a database
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
	  // 注意这一对
    group.enter()
    DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
        group.leave()
        print("leaving second group after 3 seconds")
    }
}

group.notify(queue: .main) {
    print("finished")
}

修改后:

let group = DispatchGroup()
// 注意这里 提前写好两对 group.enter()
group.enter() // user defaults call
group.enter() // database call

// Hitting something fast, like UserDefaults
DispatchQueue.main.async {
    group.leave()
    print("leaving first group almost immediately")
}

// Hitting something slow, like a database
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
    DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
        group.leave()
        print("leaving second group after 3 seconds")
    }
}

group.notify(queue: .main) {
    print("finished")
}

5. 关于锁

os_unfair_lock 在系统上来说是最快的锁,它优先级更好,上下文切换更少。但是在 Swift 中不进行包装使用是不安全的,事实上,默认情况下 pthread_mutex 已更改为使用unfair locking ,而NSLock 本身又是使用pthread_mutex,所以在 Swift 中,使用NSLock 锁定是最佳选择。

6. 不要使用 semaphores 信号量来等待异步工作

dispatch_queue_t q = ...;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_async(q,^ {dispatch_semaphore_signal(sema);});
dispatch_semaphore_wait(sema,DISPATCH_TIME_FOREVER);

当此代码的调用者阻塞了一个工作线程时,内核将注意到您的并发级别降低,并为您带来一个新线程。这个线程很可能是捡起被异步唤醒的q的线程,并将取消阻塞调用者。

很多文章教程其实还是用semaphores,目前我觉得如果真的这样糟糕,可以用DispatchGroup代替吧

7. 那我们应该怎么做

  • 将你的App分为几个子系统(UI,数据库,网络等),并为每个子系统分配有限数量的串行队列,最好是一个。
  • 让每个子系统只有一个队列是困难的,可以借助GCD target queues 创建队列层次结构,这些是有规模的队列,可以创建多少就多少
  • 不要将工作分配到全局队列,而是正确的使用每一个子系统的队列们
  • 需要单一底层资源(如网络控制器或其他物理硬件)的工作可能不适合并行化
  • 如果发现App存在性能瓶颈,并且这项工作很适合并行化,那可以度量和调整线程参数以获得最佳性能。在您支持的设备的最高端和最低端执行此操作

参考

基础