GCD相关知识第一篇
demo地址:https://github.com/TinySungo/GCDProject,demo中列表选择“队列”查看输出结果,文中相应代码在QueueViewController.swift
有描述不清或错误的地方,请在评论处指出,或者发送至邮箱 [email protected],或者github上提issue,
延时执行
GCD 可以通过asyncAfter提交一个延时操作到指定线程中
比如:点击按钮延时三秒输出
@objc func printSomthing() {
print("点击了按钮")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
print("这句话是在点击按钮3秒之后输出的")
}
print("在主线程执行的任务,不被阻塞")
}
执行输出顺序:
点击了按钮
在主线程执行的任务,不被阻塞
这句话是在点击按钮3秒之后输出的
倒计时
在GCD中计时器timer通过DispatchSource的makeTimerSource方法来创建,注意参数queue就是回调的执行环境(队列)
方案一:
@objc func startCountDown() {
print("开始倒计时")
self.button.isEnabled = false
// 创建一个倒计时, 指定queue为global,EventHandler在global线程执行
let timer = DispatchSource.makeTimerSource(flags: .init(rawValue: 0), queue: DispatchQueue.global())
timer.schedule(deadline: .now(), repeating: .milliseconds(1000)) // 毫秒计
timer.setEventHandler {
self.totalTime = self.totalTime - 1
if self.totalTime < 0 {
timer.cancel()
// UI操作必须切回到主线程
DispatchQueue.main.async {
self.button.setTitle("点击开始倒计时", for: .normal)
self.button.isEnabled = true
}
} else {
DispatchQueue.main.async {
self.button.setTitle("\(self.totalTime)", for: .normal)
}
}
}
// 激活timer
timer.activate()
}
以上代码实现了简单的倒计时功能,但是有一个问题,当程序进入后台时,倒计时就会出错(原因是程序进入后台,会挂起线程),如何解决这个问题呢?
方案二:
@objc func startCountDown() {
print("开始倒计时")
self.button.isEnabled = false
let endTime = NSDate.init(timeIntervalSinceNow: totalTime)
// 创建一个倒计时, 指定queue为global,EventHandler在global线程执行
let timer = DispatchSource.makeTimerSource(flags: .init(rawValue: 0), queue: DispatchQueue.global())
timer.schedule(deadline: .now(), repeating: 1)
// 比如下面这个方法的意思就是1秒后开始倒计时,每1秒重复执行,容差10毫秒
// timer.schedule(deadline: .now() + 1, repeating: 1, leeway: .milliseconds(10))
timer.setEventHandler {
let interval = Int(endTime.timeIntervalSinceNow)
if interval <= 0 {
timer.cancel()
DispatchQueue.main.async {
self.button.setTitle("点击开始倒计时", for: .normal)
self.button.isEnabled = true
}
} else {
print("还有:" + "\(interval)")
DispatchQueue.main.async {
self.button.setTitle("\(interval)", for: .normal)
}
}
}
timer.resume()
}
那为什么方案二就可以解决方案一中提到的问题呢?
仔细看以下两行代码:其实是利用NSDate本机时间模拟了服务器时间
let endTime = NSDate.init(timeIntervalSinceNow: totalTime)
let interval = Int(endTime.timeIntervalSinceNow)
看到这里,有些小可爱已经发现问题了,方案二在用户改变系统时间的时候,又是会有bug了,当然最严谨的方案肯定是通过获取服务器时间的方式
继续看下去
✨队列
打个比方,食堂排队打饭。一个窗口会有一条队伍(线程)排着,每个队伍互不相干(并行队列),食堂只有3个窗口(队列的最大并发量是3),先排队的人先能打到饭了(队列是FIFO的数据结构)。
同步执行:
一个阿姨在一个窗口给打饭(会阻塞当前线程,也就是说这个队伍中有一个人正在打饭,后面的人都得等着)
异步执行:
有多个阿姨在一个窗口给打饭(可以同时执行多个任务)
注意⚠️:队列真的真的和同步异步没啥关系,同步异步指的那是队列中任务的执行方式。
队列分为以下四种:
- 串行队列:前面提到的排队打饭,如果食堂只开一个窗口,这时候串行队列了。
- 并行队列: 开启多个线程,同时执行任务
- 主队列:特殊的串行队列,必须有一个主线程。⚠️主线程里也有任务必须等主线程任务执行完才能轮到主队列
- 全局队列:特殊的并发队列
执行方式分为以下两种:
- 同步执行:阻塞当前线程
- 异步执行:立即返回,不会阻塞当前线程
两两组合8种情况,分别来看一下~
先介绍一下会用到模拟耗时操作的方法,打印任务执行的线程,sleep(2)模拟耗时
/// 这是用于模拟耗时操作的输出
///
/// - Parameter number: 第几个加入的任务
func readData(_ number: Int) {
print(Thread.current)
print("start " + "\(number)")
sleep(2)
print("end " + "\(number)")
}
1、串行队列,同步执行
func test1() {
let serialQueue = DispatchQueue(label: "com.MT.serialQueue")
serialQueue.sync {
readData(0)
}
serialQueue.sync {
readData(1)
}
print("当前线程执行的任务")
}
输出
{number = 1, name = main}
start 0
end 0
{number = 1, name = main}
start 1
end 1
当前线程执行的任务
结论:任务都在主线程执行,没有创建新线程;阻塞当前线程
**应用:FMDB 保证数据的读写安全 **
2、串行队列,异步执行
func test2() {
let serialQueue = DispatchQueue(label: "com.MT.serialQueue")
serialQueue.async {
self.readData(0)
}
serialQueue.async {
self.readData(1)
}
print("当前线程执行的任务")
}
输出
当前线程执行的任务
{number = 3, name = (null)}
start 0
end 0
{number = 3, name = (null)}
start 1
end 1
主线程按顺序提交任务0、1到serialQueue,在serialQueue上依次执行,主线程和这个新的线程是并行的,相互不影响
结论:创建了一条新线程,只有一条!!!,任务按照提交顺序在新线程中执行;不阻塞当前线程
3、并行队列,同步执行
func test3() {
let concurrntQueue = DispatchQueue(label: "com.MT.concrrentQueue", attributes: .concurrent)
concurrntQueue.sync {
self.readData(0)
}
concurrntQueue.sync {
self.readData(1)
}
print("当前线程执行的任务")
}
输出
{number = 1, name = main}
start 0
end 0
{number = 1, name = main}
start 1
end 1
当前线程执行的任务
这里要看一下了。输出结果怎么和 串行队列,同步执行 是一样的呢?
结合打饭模型解释就很容易理解,并行队列(排了多个队伍),异步执行(只有一个窗口)。排队排那么多没有啊,窗口只有那么一个,也就是说所有的任务,最终都是按照顺序提交到了主线程中
也就是说:同步执行任务意味着阻塞当前线程,不论这个任务在哪一种队列中
结论:任务都在主线程执行,没有创建新线程;阻塞当前线程
4、并行队列,异步执行
func test4() {
let concurrntQueue = DispatchQueue(label: "com.MT.concrrentQueue", attributes: .concurrent)
concurrntQueue.async {
self.readData(0)
}
concurrntQueue.async {
self.readData(1)
}
concurrntQueue.async {
self.readData(2)
}
print("当前线程执行的任务")
}
输出(运行两次,观察两次的结果)
当前线程执行的任务
{number = 3, name = (null)}
{number = 4, name = (null)}
{number = 5, name = (null)}
start1
start2
start0
end 1
end 0
end 2
当前线程执行的任务
{number = 6, name = (null)}
{number = 7, name = (null)}
{number = 8, name = (null)}
start0
start1
start2
end 0
end 1
end 2
开了多条新线程,执行顺序也是乱序的
结论:创建多条新线程,任务并发执行;不阻塞当前线程
5、全局队列,同步执行
func test5() {
let globalQueue = DispatchQueue.global()
globalQueue.sync {
self.readData(0)
}
globalQueue.sync {
self.readData(1)
}
print("当前线程执行的任务")
}
输出
{number = 1, name = main}
start0
end 0
{number = 1, name = main}
start1
end 1
当前线程执行的任务
6、全局队列,异步执行
func test6() {
let globalQueue = DispatchQueue.global()
globalQueue.async {
self.readData(0)
}
globalQueue.async {
self.readData(1)
}
globalQueue.async {
self.readData(2)
}
print("当前线程执行的任务")
}
输出
当前线程执行的任务
{number = 3, name = (null)}
{number = 4, name = (null)}
{number = 5, name = (null)}
start2
start1
start0
end 2
end 1
end 0
全局队列是特殊特殊的并行队列,全局队列由系统提供,可以通过DispatchQueue.global() 直接获取,而3、4中的并行队列由自己创建。
所以5、6的结论是对应3、4
7、主队列,同步执行
func test7() {
let mainQueue = DispatchQueue.main
mainQueue.sync {
self.readData(0)
}
mainQueue.sync {
self.readData(1)
}
print("当前线程执行的任务")
}
奔溃!Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
分析一下:首先明确一点,主队列永远在主线程中工作。 同步执行,意味着阻塞当前线程(主线程),主线程在等着任务0执行完毕,但是任务0也是在主线程中执行的,于是等啊等一直等不到任务结束的信号,造成死锁!!! 看奔溃信息也可以看到,奔溃在了re_dispatch_sync_wait ()
❓问题1:如果将上述test7()改成以下代码,会发生什么?(答案在文末)
DispatchQueue.global().async {
print("currentThread: \( Thread.current)")
self.test7()
}
❓问题2:改成这样又如何呢?
DispatchQueue.global().sync {
print("currentThread: \( Thread.current)")
self.test7()
}
8、主队列,异步执行
func test8() {
let mainQueue = DispatchQueue.main
mainQueue.async {
self.readData(0)
}
mainQueue.async {
self.readData(1)
}
mainQueue.async {
self.readData(2)
}
print("当前线程执行的任务")
}
输出
当前线程执行的任务
{number = 1, name = main}
start0
end 0
{number = 1, name = main}
start1
end 1
{number = 1, name = main}
start2
end 2
结论:不创建新线程,所有任务在主线程按顺序完成;不阻塞当前线程
综上:
- 是否阻塞当前线程,看任务执行方式 (同步=>阻塞,异步=>不阻塞)
- 是否开辟新的线程,看任务执行方式 (同步=>不开,异步=>开新)
- 如果异步执行开多少条线程,看队列 (串行=>一条,并行=>多条)
- 主队列要特例!主队列的任务永远在主线程中执行。
最后回答一下上面抛出的两个问题:
问题1:
DispatchQueue.global().async {
/// print
print("currentThread: \( Thread.current)")
/// test7
let mainQueue = DispatchQueue.main
mainQueue.sync {
self.readData(0)
}
mainQueue.sync {
self.readData(1)
}
print("当前线程执行的任务")
}
分析代码,套了一层DispatchQueue.global().async{} 这个之后,就是全局队列,异步执行,里面的任务(print 和 执行test7()方法)在新的线程ThreadA中执行,并且不会阻塞当前线程。
下面来看test7()方法:将任务0、1放到主线程中同步执行,顺序执行,但是阻塞当前线程threadA。
可以预测输出结果的顺序应当是:
currentThread: ThreadA
{number = 1, name = main}
start0
end 0
{number = 1, name = main}
start1
end 1
当前线程执行的任务
run一下验证输出:
currentThread: {number = 3, name = (null)}
{number = 1, name = main}
start0
end 0
{number = 1, name = main}
start1
end 1
当前线程执行的任务
其实把问题1的代码简化一下,就是在实际开发中经常会用的“套件”。
下面这样写,是不是就很熟悉了_
print("主线程中操作1")
DispatchQueue.global().async {
/// TODO: 耗时操作
/// ....
DispatchQueue.main.sync {
print("回到主线程更新UI等操作...")
}
///
}
print("主线程中的操作2")
当前也可以用 DispatchQueue.main.async{},区别在于是否会阻塞当前的全局队列
问题2:
DispatchQueue.global().sync {
/// print
print("currentThread: \( Thread.current)")
/// test7
let mainQueue = DispatchQueue.main
mainQueue.sync {
self.readData(0)
}
mainQueue.sync {
self.readData(1)
}
print("当前线程执行的任务")
}
仔细看两两组合的第3重情况(并行队列,同步执行)分析,当前线程还是主线程,所以,套一层 DispatchQueue.global().sync {} 根本没有意义,依然 死锁!!!