如何理解 iOS 中的串行队列和并行队列?

721 阅读4分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

前言

在 iOS 中,当我们编写一些耗时较多的任务时,通常的做法是把这些任务放到子线程去执行,来保证 APP 的流畅性。

多线程的代码编写十分复杂,需要开发者去处理线程的生命周期。Apple 为了规避这一问题,开发了 GCD 这一技术框架,它可以使开发者专注于任务代码的编写。而串行队列和并行队是 GCD 这一技术的很重要的两个概念,所以正确的理解它们还是很重要的。

在开始讲串行队列和并行队列之前,先来了解一下派发队列(DispatchQueue)。

派发队列

Dispatch 是对 GCD 队列的一种抽象,它可以让你在代码中去同步或者异步的去执行你的任务。需要注意的是,任务总是按照被添加到队列的顺序执行。

串行队列

同一时间,只能执行一个任务的队列,称之为串行队列。串行队列通常用于数据同步,比如 iOS 中主队列就是串行队列。

  • 创建串行队列:
let serialQ = DispatchQueue.init(label: "test.serial.queue")
serialQ.async {
    print("Task 1 begin")
    print("Task 1 end")
}

serialQ.async {
    print("Task 2 begin")
    print("Task 2 end")
}

//Task 1 begin
//Task 1 end
//Task 2 begin
//Task 2 end

从结果可以看出,Task2 是在 Task1 结束之后执行的。

并行队列

同一时间,可以多个任务一起执行的队列,称之为并行队列。上文提到,任务总是按照添加到队列的顺序执行,对于并行队列也是如此,但它们的结束顺序是不确定的,因为任务是并行执行的。

在并行队列中,任务会在不同的线程上去执行,这些线程由 GCD 来管理,无需我们开发者去关心。

虽然并行队列可以同时执行多个任务,但任务个数还是有限制的,不能无限大,它的个数依赖于运行的硬件设备。在开发中,我们一般线程数会控制在 3-5 之间。

  • 创建并行队列
let concurrentQ = DispatchQueue.init(label: "test.concurrent.queue", attributes: .concurrent)

concurrentQ.async {
    print("Task 1 begin")
    print("Task 1 end")
}

concurrentQ.async {
    print("Task 2 begin")
    print("Task 2 end")
}

//Task 1 begin
//Task 2 begin
//Task 1 end
//Task 2 end

由打印可以看出,在 Task1 结束之前,Task2 已经开始执行了。

在项目中,正确的使用并行队列,可以提高代码性能,提升用户的使用体验。如果使用不正确呢?会造成什么问题呢?

并行的问题

试想一种场景,当多条线程去同时修改一块内存的数据,会造成什么问题?

这会造成多线程最常见的问题:数据竞争。这会给项目带来无可预知且不易复现的 bug。会使开发者掉很多头发,所以为了我们为数不多的头发,应该避免写有问题的代码😏。

数据竞争的问题解决办法就是在修改数据时应进行同步操作,即修改数据时应仅有一条线程。

在 iOS 中可以通过加锁、信号量、barrier 等方式来进行同步。

  • 通过加锁规避:
let lock = NSLock()

concurrentQ.async {
    lock.lock()
    num += 10
    lock.unlock()
}

同步与异步

添加到派发队列的任务有两种执行方式:同步与异步。

  • 同步:当队列中的上一个任务执行完,才会执行当前任务。
  • 异步:无需等待上一个任务执行完,即可执行当前任务。

需要特别注意的是,不要在主队列去添加同步任务,这样会阻塞主队列,导致程序 crash。

// 这会造成死锁,导致程序崩溃。
DispatchQueue.main.sync {
    print("abc")
}

既然这里提到了主队列,那下面就简单介绍一下主队类。

主队列

在 iOS 中,主队列是一种特殊的、全局的串行队列。添加到主队列的任务都会在主线程执行。而主线程是负责 UI 的交互的。

所以,任何繁重的工作都不应该添加到主队列去执行,这会导致你的页面卡顿,用户体验差。

既然在主队列中添加同步任务会造成死锁,那添加异步任务呢?答案是不会。而且,通常我们在通过接口获取网络数据后,会通过下面的代码回到主队列,去刷新 UI 页面。

concurrentQ.async {
    getData()
    DispatchQueue.main.async {
        // 刷新 UI
    }
}

既然,iOS 中有一个全局的串行队列,那它有没有全局的并行队列呢?

Global

答案自然是有。global 就是全局的并行队列。

  • global
DispatchQueue.global().async {
    
}

所以,在大部分情况下我们并不需要去手动创建队列。正确的使用两个全局的队列足可应付大部分开发需求。

总结

  • 串行队列同一时间只能执行一个任务;并行队列同一时间可以执行多个任务。
  • 同步:需要等待上一个任务结束完,才能执行当前任务;异步:无需等待即可执行当前任务。
  • 千万不要在主队列添加同步任务。
  • 线程并不是开的越多越好。
  • 多线程环境需要注意数据竞争问题。