「这是我参与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 {
}
所以,在大部分情况下我们并不需要去手动创建队列。正确的使用两个全局的队列足可应付大部分开发需求。
总结
- 串行队列同一时间只能执行一个任务;并行队列同一时间可以执行多个任务。
- 同步:需要等待上一个任务结束完,才能执行当前任务;异步:无需等待即可执行当前任务。
- 千万不要在主队列添加同步任务。
- 线程并不是开的越多越好。
- 多线程环境需要注意数据竞争问题。