优先级反转那些事儿

作者:崔晓兵

从一个线上问题说起

最近在线上遇到了一些[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;
}

值得注意的是,这里只能使用pthreadapiNSThread提供的API是不可行的

Demo 验证

为了验证上述的手动调整线程优先级是否有一定的效果,这里通过demo进行本地实验:定义了2000operation(目的是为了CPU繁忙),优先级设置NSQualityOfServiceUserInitiated,且对其中可以被100整除的operation的优先级调整为NSQualityOfServiceBackground,在每个operation执行相同的耗时任务,然后对这被选中的10operation进行耗时统计。

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

可以看到

  1. 正常情况下,每个任务的平均耗时为:11.8190561;
  2. operation被设置为低优先级时,其耗时大幅度提升为:94.70210189;
  3. 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. 第一个阶段如第1个红框所示,从36号开始在版本19.7上有较大幅度的下降,主要原因:堆栈中被等待的队列信息由QOS:BACKGROUND变为了com.apple.root.default-qos,队列的优先级从QOS_CLASS_BACKGROUND提升为QOS_CLASS_DEFAULT,相当于实施了方案一,使用了默认优先级。
  2. 第二个阶段如第2个红框所示,从424号在版本20.3上开始验证。目前看起来效果暂时不明显,推测一个主要原因是:demo中是把优先级从QOS_CLASS_BACKGROUND提升为QOS_CLASS_USER_INITIATED,而线上相当于把队列的优先级从默认的优先级QOS_CLASS_DEFAULT提升为QOS_CLASS_USER_INITIATED所以相对来说,线上的提升相对有限。
    1. QOS_CLASS_BACKGROUNDMach层级优先级数是4;
    2. QOS_CLASS_DEFAULTMach层级优先级数是31;
    3. QOS_CLASS_USER_INITIATEDMach层级优先级数是37;

深刻理解优先级反转

那么是否所有锁都需要像上文一样,手动提升持有锁的线程优先级?系统是否会自动调整线程的优先级?如果有这样的机制,是否可以覆盖所有的锁?要理解这些问题,需要深刻认识

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值