在进行iOS司机端溃散治理时,发现司机端账号中心里边相关模型比方司机信息模型,里边的多个特点,比方phone_nocity_id等多个字段,都存在多线程安全问题,即在主线程更新值,在子线程继续拜访的野指针溃散问题。

由于涉及到的字段相对较多,当时为了确保后边其他的字段也能规避多线程安全问题,决定利用并发行列,来对模型里边的特点,完成读写锁,来确保线程安全。

就相似如下:

自定义并发队列实现读写锁的死锁问题记录

然后提测阶段,测试性能压测出现了卡死被watchdog强杀的溃散。依据溃散日志,检查是在司机信息的location_city_name.getter这儿卡死导致的。

自定义并发队列实现读写锁的死锁问题记录

二. 原因排查

这儿我一向无法理解,按道理并行行列,来完成读写锁,确保get办法在当前线程履行,而set办法,会等之前行列里边一切的get都完成之后堵塞履行完成set办法,再去履行其他操作。

自定义并发队列实现读写锁的死锁问题记录

从理论上来讲,这儿并不存在死锁等候的可能性。

因而我写了一个demo测试:

Demo如下:

自定义并发队列实现读写锁的死锁问题记录

自定义并发队列实现读写锁的死锁问题记录

从输出日志能够看出:

自定义并发队列实现读写锁的死锁问题记录

这儿只输出了print("--------------Thread VC: (Thread.current)")而没有输出后边的print("--------------------------------------"), 也就证明这儿存在死锁,导致后边的使命不会履行。

这是由于都是放在子线程里边进行操作,所以主线程仍然是能够履行,因而UI事件也是能够呼应的,因而咱们点击,调用点击事件:

 // MARK: - Actions
    @objc
    func buttonTapClicked(button: UIButton) {
        self.testModel.update(phoneNo: "100999988888")
    }

这是咱们发现主线程也陷入了等候中,从而卡死。

从这个例子里边咱们主要是多次异步拜访了模型里边的phoneNo特点,而这个特点的运用并行行列的读写锁,来确保phoneNo拜访的线程安全。

因而咱们进一步简化这个例子:

自定义并发队列实现读写锁的死锁问题记录

从简化的例子,咱们仍然能够观察到死锁,由于使命并没有履行结束。

自定义并发队列实现读写锁的死锁问题记录

咱们选择了其间的32线程,检查线程调用的仓库,咱们看到了

2   libdispatch.dylib                   0x0000000104cae96c _dispatch_thread_event_wait_slow + 56,
3   libdispatch.dylib                   0x0000000104cbfbe8 __DISPATCH_WAIT_FOR_QUEUE__ + 384,
4   libdispatch.dylib                   0x0000000104cbf520 _dispatch_sync_f_slow + 184,

这儿_dispatch_thread_event_wait_slow说明使命正在等候线程来履行,之所以陷入等候,是由于线程池里边的64条线程悉数被占用了,没有多余的线程来履行使命,那为什么线程池里边的线程会被悉数占用呢。

这儿需求知道一点是,自定义并发行列和大局并发行列都依赖于GCD线程池的线程去履行使命,并且一切并发行列,最多只能创立64条线程,假如这64条线程,悉数被占用,那么并发行列里边的使命,就需求等候线程,直到线程闲暇。

具体详见: GCD的串行行列、并发行列、大局并发行列创立线程数

那为什么并发行列的 64 条都被占用了,并且没有释放呢?

首要咱们看下循环之前的线程分布:

自定义并发队列实现读写锁的死锁问题记录

咱们能够看到,在调用大局行列进行读写操作之前,GCD线程池里边现已存储2、3、4、5、7、8、9这几条子线程。

然后咱们看下打印日志的输出:

自定义并发队列实现读写锁的死锁问题记录

这儿增加了current thread的日志打印。

自定义并发队列实现读写锁的死锁问题记录

  • 首要for循环履行1000DispatchQueue.global(qos: .default).async使命,这时分默认优先级大局行列会去GCD线程池获取线程来履行block里边的使命,由于GCD线程池,之前就存在几条线程,因而能够直接获取,去履行block里边的使命,这几条线程称为第一批。

自定义并发队列实现读写锁的死锁问题记录

  • 第一批线程履行完打印操作和driverInfoQueue.syncget操作后,履行到了driverInfoQueue.async(flags: .barrier)

  • 由于driverInfoQueue.async(flags: .barrier)会等到行列前面一切使命履行结束之后,才能履行,而这时分for循环的DispatchQueue.global(qos: .default).async的其他线程也现已创立,并履行了打印操作,履行到了driverInfoQueue.sync这儿。

  • 也就是说这时分driverInfoQueue.async(flags: .barrier)使命是先进行列的,而后边的driverInfoQueue.sync是后边进的行列,而这儿的driverInfoQueue.sync使命又占满了线程池的其他线程,导致线程池64条线程悉数占满。

  • 这时分driverInfoQueue.async(flags: .barrier)使命虽然在行列前面,但确一向没有线程来履行只能等候。而后边的driverInfoQueue.sync使命,由于barrier的堵塞作用,也一向处于等候中。

三. 解决方案

  • 改动加锁方案,针对DriverInfoDriverTokenInfoDriverPreferenceInfo里边的特点比方accessTokenphone_nolocation_city_id等多个字段,存在多线程安全问题的字段,运用各自独立的NSLock锁来解决。