美文网首页
GCD队列和执行方式的梳理

GCD队列和执行方式的梳理

作者: TinySungo | 来源:发表于2018-10-11 15:09 被阅读0次

    🌈GCD相关知识第一篇

    demo地址:https://github.com/TinySungo/GCDProject,demo中列表选择“队列”查看输出结果,文中相应代码在QueueViewController.swift

    有描述不清或错误的地方,请在评论处指出,或者发送至邮箱 MT_Xiao@163.com,或者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("当前线程执行的任务")
        }
    
    

    输出

    <NSThread: 0x60000007cf40>{number = 1, name = main}
    start 0
    end 0
    <NSThread: 0x60000007cf40>{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("当前线程执行的任务")
        }
    
    

    输出

    当前线程执行的任务
    <NSThread: 0x60400046a5c0>{number = 3, name = (null)}
    start 0
    end 0
    <NSThread: 0x60400046a5c0>{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("当前线程执行的任务")
        }
    

    输出

    <NSThread: 0x600000260b80>{number = 1, name = main}
    start 0
    end 0
    <NSThread: 0x600000260b80>{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("当前线程执行的任务")
        }
    

    输出(运行两次,观察两次的结果)

    当前线程执行的任务
    <NSThread: 0x600000277b40>{number = 3, name = (null)}
    <NSThread: 0x60400027e180>{number = 4, name = (null)}
    <NSThread: 0x60400027e400>{number = 5, name = (null)}
    start1
    start2
    start0
    end 1
    end 0
    end 2
    
    当前线程执行的任务
    <NSThread: 0x604000464b80>{number = 6, name = (null)}
    <NSThread: 0x600000273f80>{number = 7, name = (null)}
    <NSThread: 0x604000468d40>{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("当前线程执行的任务")
        }
    

    输出

    <NSThread: 0x600000070600>{number = 1, name = main}
    start0
    end 0
    <NSThread: 0x600000070600>{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("当前线程执行的任务")
        }
    
    

    输出

    当前线程执行的任务
    <NSThread: 0x604000668dc0>{number = 3, name = (null)}
    <NSThread: 0x600000269180>{number = 4, name = (null)}
    <NSThread: 0x604000460e40>{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("当前线程执行的任务")
        }
    

    输出

    当前线程执行的任务
    <NSThread: 0x60400007c6c0>{number = 1, name = main}
    start0
    end 0
    <NSThread: 0x60400007c6c0>{number = 1, name = main}
    start1
    end 1
    <NSThread: 0x60400007c6c0>{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
    <NSThread>{number = 1, name = main}
    start0
    end 0
    <NSThread>{number = 1, name = main}
    start1
    end 1
    当前线程执行的任务
    

    run一下验证输出:

    currentThread: <NSThread: 0x60000046e5c0>{number = 3, name = (null)}
    <NSThread: 0x60000006ee40>{number = 1, name = main}
    start0
    end 0
    <NSThread: 0x60000006ee40>{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队列和执行方式的梳理

          本文链接:https://www.haomeiwen.com/subject/ilubaftx.html