美文网首页iOS实践iOS 底层原理
iOS 线程死锁的原因和解决办法

iOS 线程死锁的原因和解决办法

作者: 栋柠柒 | 来源:发表于2021-06-20 18:14 被阅读0次

    线程死锁是个老生常谈的问题,在这里只说线程和 操作(operation)一般都是一起的,因为operation 是需要在线程当中执行的,那么就会有一段很经典的线程死锁的代码:

    override func viewDidLoad() {
            super.viewDidLoad()
            self.deadLockTest()
            debugPrint("\(Thread.current), viewDidLoad")
        }
    
    func deadLockTest() {
            DispatchQueue.main.sync {
                debugPrint("\(Thread.current), deadLockTest)")
            }
        }
    

    看看这个会输出什么,
    答案是是什么都不会输入,程序直接 崩溃了,为什么呢,我们先看一下这段代码的执行逻辑。
    viewDidLoad 这个方法默认是在主队列当中排列的,然后在主线程当中执行,这一点众所周知,无需多言,那么在 DispatchQueue.main.sync 这个是什么意思呢,分两部分,
    第一部分:DispatchQueue.main 这个代表着把block 当中的操作添加到主队列来执行,大家都知道主队列是个串行队列,只能顺序取出执行,现在主队列当中有两个操作,一个 viewDidLoad,一个就是 DispatchQueue.main.sync block 当中的操作。
    第二部分:sync 就是同步执行,就是会先阻塞当前线程,直到block 当中的代码执行完毕。
    那么 DispatchQueue.main.sync 的意思就是,在主队列这个串行队列里面添加一个 代码块,并且阻塞主线程到这个代码块执行完毕。那么为啥会死锁呢,因为这个代码块添加在了串行队列里面,它要等到 viewDidLoad 执行完毕才会执行,但是 sync 又把当前线程给 stop 了,viewDid Load 也执行不下去,所以就陷入了死锁状态。

    那么再看这段代码会怎么执行

    override func viewDidLoad() {
            super.viewDidLoad()
            self.deadLockTest()
            debugPrint("\(Thread.current), viewDidLoad")
        }
        
        func deadLockTest() {
            DispatchQueue.main.async {
                debugPrint("\(Thread.current), deadLockTest)")
            }
        }
    

    输出如下

    "<NSThread: 0x6000038b82c0>{number = 1, name = main}, viewDidLoad"
    "<NSThread: 0x6000038b82c0>{number = 1, name = main}, deadLockTest)"
    

    很奇怪,两个都是主线程的输出,难道主线程还能异步执行operation吗?
    代码稍微改动下

    override func viewDidLoad() {
            super.viewDidLoad()
            self.deadLockTest()
            debugPrint("\(Thread.current), \(Date()), viewDidLoad")
            for index in (0...9000000) {
                
            }
        }
        
        func deadLockTest() {
            DispatchQueue.main.async {
                debugPrint("\(Thread.current), \(Date()), deadLockTest)")
            }
        }
    
    "<NSThread: 0x6000010bc780>{number = 1, name = main}, 2021-06-20 09:47:10 +0000, viewDidLoad"
    "<NSThread: 0x6000010bc780>{number = 1, name = main}, 2021-06-20 09:47:15 +0000, deadLockTest)"
    

    由此可以得出结论,主队列里面的操作是顺序执行的,并且只在主线程执行,一个执行完了才会执行下一个,DispatchQueue.main.async 这个方法不会开启新的线程。
    同时也可以得出另一个猜想:发生线程死锁的条件是,在同一个串行队列里面添加了同步执行的任务。
    这次不用主队列,换用自定义的队列试试。

    override func viewDidLoad() {
            super.viewDidLoad()
            self.deadLockTest()
            debugPrint("\(Thread.current), \(Date()), viewDidLoad")
        }
        
        func deadLockTest() {
            let syncQueue = dispatch_queue_serial_t(label: "syncQueue")
            syncQueue.async {
                syncQueue.sync {
                    debugPrint("\(Thread.current), \(Date()), deadLockTest sync)")
                }
                debugPrint("\(Thread.current), \(Date()), deadLockTest)")
            }
        }
    

    结果依然是 crash,因为 syncQueue 是一个串行队列,而且这个队列里面之前已有的操作没有执行完的时候向串行队列里面添加一个同步执行的操作,就会造成死锁。
    怎么样不死锁呢

    override func viewDidLoad() {
            super.viewDidLoad()
            self.deadLockTest()
            debugPrint("\(Thread.current), \(Date()), viewDidLoad")
        }
        
        func deadLockTest() {
            let syncQueue = dispatch_queue_serial_t(label: "syncQueue")
            syncQueue.async {
                syncQueue.async {
                    debugPrint("\(Thread.current), \(Date()), deadLockTest sync)")
                }
                debugPrint("\(Thread.current), \(Date()), deadLockTest)")
            }
        }
    
    "<NSThread: 0x600000294080>{number = 1, name = main}, 2021-06-20 10:01:07 +0000, viewDidLoad"
    "<NSThread: 0x60000029c040>{number = 4, name = (null)}, 2021-06-20 10:01:07 +0000, deadLockTest)"
    "<NSThread: 0x60000029c040>{number = 4, name = (null)}, 2021-06-20 10:01:07 +0000, deadLockTest sync)"
    

    串行队列问题解决了,并发队列会不会有这样的问题呢

    试一下

     override func viewDidLoad() {
            super.viewDidLoad()
            self.deadLockTest()
            debugPrint("\(Thread.current), \(Date()), viewDidLoad")
        }
        
        func deadLockTest() {
            let concurrentQueue = dispatch_queue_concurrent_t(label: "concurrentQueue")
            concurrentQueue.sync {
                concurrentQueue.sync {
                    debugPrint("\(Thread.current), \(Date()), deadLockTest sync)")
                }
                debugPrint("\(Thread.current), \(Date()), deadLockTest)")
            }
        }
    

    还是 crash 了,是以上得出的 发生线程死锁的条件是,在同一个串行队列里面添加了同步执行的任务。 这个猜想并不准确吗,因为这种情况在并发队列也会出现,
    到这里我陷入了思考,找了一下队列和线程的关系:
    1.串行队列+同步任务:不会开启新的线程,任务逐步完成。

    2.串行队列+异步任务:开启新的线程,任务逐步完成。

    3.并发队列+同步任务:不会开启新的线程,任务逐步完成。

    4.并发队列+异步任务:开启新的线程,任务同步完成。
    简而言之就是,并发队列+异步任务,才能真正达到并发的效果,那么修改一下代码
    现在改下代码

     override func viewDidLoad() {
            super.viewDidLoad()
            self.deadLockTest()
            debugPrint("\(Thread.current), \(Date()), viewDidLoad")
        }
        
        func deadLockTest() {
            let concurrentQueue = dispatch_queue_concurrent_t(label: "concurrentQueue")
            concurrentQueue.sync {
                concurrentQueue.async {
                    debugPrint("\(Thread.current), \(Date()), deadLockTest sync)")
                }
                debugPrint("\(Thread.current), \(Date()), deadLockTest)")
            }
        }
    
    "<NSThread: 0x600003a10380>{number = 1, name = main}, 2021-06-20 10:06:06 +0000, deadLockTest)"
    "<NSThread: 0x600003a10380>{number = 1, name = main}, 2021-06-20 10:06:06 +0000, viewDidLoad"
    "<NSThread: 0x600003a5a4c0>{number = 4, name = (null)}, 2021-06-20 10:06:06 +0000, deadLockTest sync)"
    

    那么最后的结论就是

    发生线程死锁的条件是,在队列里面的任务没有执行完毕的时候,在同一个队列里面添加了同步执行的任务.

    以上,仅作笔记。

    相关文章

      网友评论

        本文标题:iOS 线程死锁的原因和解决办法

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