在进行iOS司机端崩溃治理时,发现司机端账号中心里面相关模型比如司机信息模型,里面的多个属性,比如phone_no、city_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条线程,全部被占用,那么并发队列里面的任务,就需要等待线程,直到线程空闲。
那为什么并发队列的 64 条都被占用了,而且没有释放呢?
首先我们看下循环之前的线程分布:
我们可以看到,在调用全局队列进行读写操作之前,GCD线程池里面已经存储了2、3、4、5、7、8、9这几条子线程。
然后我们看下打印日志的输出:
这里增加了current thread的日志打印。
- 首先
for循环执行1000个DispatchQueue.global(qos: .default).async任务,这时候默认优先级全局队列会去GCD线程池获取线程来执行block里面的任务,由于GCD线程池,之前就存在几条线程,因此可以直接获取,去执行block里面的任务,这几条线程称为第一批。
-
第一批线程执行完打印操作和
driverInfoQueue.sync的get操作后,执行到了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的阻塞作用,也一直处于等待中。
三. 解决方案
- 改变加锁方案,针对
DriverInfo、DriverTokenInfo、DriverPreferenceInfo里面的属性比如accessToken、phone_no、location_city_id等多个字段,存在多线程安全问题的字段,使用各自独立的NSLock锁来解决。