GCD队列和执行方式的梳理

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的数据结构)。

同步执行:
一个阿姨在一个窗口给打饭(会阻塞当前线程,也就是说这个队伍中有一个人正在打饭,后面的人都得等着)

异步执行:
有多个阿姨在一个窗口给打饭(可以同时执行多个任务)

注意⚠️:队列真的真的和同步异步没啥关系,同步异步指的那是队列中任务的执行方式。

队列分为以下四种:

  1. 串行队列:前面提到的排队打饭,如果食堂只开一个窗口,这时候串行队列了。
  2. 并行队列: 开启多个线程,同时执行任务
  3. 主队列:特殊的串行队列,必须有一个主线程。⚠️主线程里也有任务必须等主线程任务执行完才能轮到主队列
  4. 全局队列:特殊的并发队列

执行方式分为以下两种:

  1. 同步执行:阻塞当前线程
  2. 异步执行:立即返回,不会阻塞当前线程

两两组合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. 是否阻塞当前线程,看任务执行方式 (同步=>阻塞,异步=>不阻塞)
  2. 是否开辟新的线程,看任务执行方式 (同步=>不开,异步=>开新)
  3. 如果异步执行开多少条线程,看队列 (串行=>一条,并行=>多条)
  4. 主队列要特例!主队列的任务永远在主线程中执行。

最后回答一下上面抛出的两个问题:

问题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 {} 根本没有意义,依然 死锁!!!

你可能感兴趣的:(GCD队列和执行方式的梳理)