作者:崔晓兵
从一个线上问题说起
最近在线上遇到了一些[HMDConfigManager remoteConfigWithAppID:]
卡死
初步分析
观察了下主线程堆栈,用到的锁是读写锁[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q9tQjjAd-1669621412273)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/50e8b46fed714d2788a31446a192afaatplv-k3u1fbpfcp-zoom-1.image)]随后又去翻了下持有着锁的子线程,有各种各样的情况,且基本都处于正常的执行状态,例如有的处于打开文件状态,有的处于`read`状态,有的正在执行`NSUserDefaults`的方法…[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AGQOke1N-1669621412275)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3cfa4df0de8c44b48bc462e1ed891f10tplv-k3u1fbpfcp-zoom-1.image)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7yKhbpG2-1669621412276)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9817c2ac51114fa9a8ce523761b88407tplv-k3u1fbpfcp-zoom-1.image)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-95aOc7Pj-1669621412277)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6a1f49b1e07041ff89d59fc35e0b78d7tplv-k3u1fbpfcp-zoom-1.image)]通过观察发现,出问题的线程都有QOS:BACKGROUND
标记。整体看起来持有锁的子线程仍然在执行,只是留给主线程的时间不够了。为什么这些子线程在持有锁的情况下,需要执行这么久,直到主线程的8s卡死?一种情况就是真的如此耗时,另一种则是出现了优先级反转。
解决办法
在这个案例里面,持有读写锁且优先级低的线程迟迟得不到调度(又或者得到调度的时候又被抢占了,或者得到调度的时候时间已然不够了),而具有高优先级的线程由于拿不到读写锁,一直被阻塞,所以互相死锁。iOS8
之后引入了QualityOfService
的概念,类似于线程的优先级,设置不同的QualityOfService
的值后系统会分配不同的CPU
时间、网络资源和硬盘资源等,因此我们可以通过这个设置队列的优先级 。
方案一:去除对NSOperationQueue
的优先级设置
在 Threading Programming Guide 文档中,苹果给出了提示:
Important: It is generally a good idea to leave the priorities of your threads at their default values. Increasing the priorities of some threads also increases the likelihood of starvation among lower-priority threads. If your application contains high-priority and low-priority threads that must interact with each other, the starvation of lower-priority threads may block other threads and create performance bottlenecks.
苹果的建议是不要随意修改线程的优先级,尤其是这些高低优先级线程之间存在临界资源竞争的情况。所以删除相关优先级设置代码即可解决问题。
方案二:临时修改线程优先级
在 pthread_rwlock_rdlock(3pthread) 发现了如下提示:
Realtime applications may encounter priority inversion when using read-write locks. The problem occurs when a high priority thread “locks” a read-write lock that is about to be “unlocked” by a low priority thread, but the low priority thread is preempted by a medium priority thread. This scenario leads to priority inversion; a high priority thread is blocked by lower priority threads for an unlimited period of time. During system design, realtime programmers must take into account the possibility of this kind of priority inversion. They can deal with it in a number of ways, such as by having critical sections that are guarded by read-write locks execute at a high priority, so that a thread cannot be preempted while executing in its critical section.
尽管针对的是实时系统,但是还是有一些启示和帮助。按照提示,对有问题的代码进行了修改:在线程通过pthread_rwlock_wrlock
拿到_rwlock
的时候,临时提升其优先级,在释放_rwlock
之后,恢复其原先的优先级。
- (id)remoteConfigWithAppID:(NSString *)appID
{
.......
pthread_rwlock_rdlock(&_rwlock);
HMDHeimdallrConfig *result = ....... // get existing config
pthread_rwlock_unlock(&_rwlock);
if(result == nil) {
result = [[HMDHeimdallrConfig alloc] init]; // make a new config
pthread_rwlock_wrlock(&_rwlock);
qos_class_t oldQos = qos_class_self();
BOOL needRecover = NO;
// 临时提升线程优先级
if (_enablePriorityInversionProtection && oldQos < QOS_CLASS_USER_INTERACTIVE) {
int ret = pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0);
needRecover = (ret == 0);
}
......
pthread_rwlock_unlock(&_rwlock);
// 恢复线程优先级
if (_enablePriorityInversionProtection && needRecover) {
pthread_set_qos_class_self_np(oldQos, 0);
}
}
return result;
}
值得注意的是,这里只能使用
pthread
的api
,NSThread
提供的API
是不可行的
Demo 验证
为了验证上述的手动调整线程优先级是否有一定的效果,这里通过demo
进行本地实验:定义了2000
个operation
(目的是为了CPU
繁忙),优先级设置NSQualityOfServiceUserInitiated
,且对其中可以被100
整除的operation
的优先级调整为NSQualityOfServiceBackground
,在每个operation
执行相同的耗时任务,然后对这被选中的10
个operation
进行耗时统计。
for (int j = 0; j < 2000; ++j) {
NSOperationQueue *operation = [[NSOperationQueue alloc] init];
operation.maxConcurrentOperationCount = 1;
operation.qualityOfService = NSQualityOfServiceUserInitiated;
// 模块1
// if (j % 100 == 0) {
// operation.qualityOfService = NSQualityOfServiceBackground;
// }
// 模块1
[operation addOperationWithBlock:^{
// 模块2
// qos_class_t oldQos = qos_class_self();
// pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED, 0);
// 模块2
NSTimeInterval start = CFAbsoluteTimeGetCurrent();
double sum = 0;
for (int i = 0; i < 100000; ++i) {
sum += sin(i) + cos(i) + sin(i*2) + cos(i*2);
}
start = CFAbsoluteTimeGetCurrent() - start;
if (j % 100 == 0) {
printf("%.8f\n", start * 1000);
}
// 模块2
// pthread_set_qos_class_self_np(oldQos, 0);
// 模块2
}];
}
统计信息如下图所示
A | B | C |
---|---|---|
(注释模块1和模块2代码) | (只打开模块1代码) | (同时打开模块1和模块2代码) |
11.8190561 | 94.70210189 | 15.04005137 |
可以看到
- 正常情况下,每个任务的平均耗时为:11.8190561;
- 当
operation
被设置为低优先级时,其耗时大幅度提升为:94.70210189; - 当
operation
被设置为低优先级时,又在Block
中手动恢复其原有的优先级,其耗时已经大幅度降低:15.04005137( 耗时比正常情况高,大家可以思考下为什么)
通过Demo
可以发现,通过手动调整其优先级,低优先级任务的整体耗时得到大幅度的降低,这样在持有锁的情况下,可以减少对主线程的阻塞时间。
上线效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ILBP87yd-1669621412277)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0b8f42b77c98405c918d31dda060be1b~tplv-k3u1fbpfcp-zoom-1.image)]
该问题的验证过程分为2
个阶段:
- 第一个阶段如第1个红框所示,从
3
月6
号开始在版本19.7
上有较大幅度的下降,主要原因:堆栈中被等待的队列信息由QOS:BACKGROUND
变为了com.apple.root.default-qos
,队列的优先级从QOS_CLASS_BACKGROUND
提升为QOS_CLASS_DEFAULT
,相当于实施了方案一,使用了默认优先级。 - 第二个阶段如第
2
个红框所示,从4
月24
号在版本20.3
上开始验证。目前看起来效果暂时不明显,推测一个主要原因是:demo
中是把优先级从QOS_CLASS_BACKGROUND
提升为QOS_CLASS_USER_INITIATED
,而线上相当于把队列的优先级从默认的优先级QOS_CLASS_DEFAULT
提升为QOS_CLASS_USER_INITIATED
所以相对来说,线上的提升相对有限。QOS_CLASS_BACKGROUND
的Mach
层级优先级数是4;QOS_CLASS_DEFAULT
的Mach
层级优先级数是31;QOS_CLASS_USER_INITIATED
的Mach
层级优先级数是37;
深刻理解优先级反转
那么是否所有锁都需要像上文一样,手动提升持有锁的线程优先级?系统是否会自动调整线程的优先级?如果有这样的机制,是否可以覆盖所有的锁?要理解这些问题,需要深刻认识优先