当前位置: 首页 > news >正文

「iOS」————优先级反转

iOS学习

    • 优先级反转问题
      • 优先级反转的后果
        • Bounded priority inversion
        • Unbounded priority inversion
        • 解决方法
          • 面试题


优先级反转问题

什么是优先级反转?

优先级反转是指:某同步资源被较低优先级的进程/线程所拥有,较高优先级的线程/进程竞争改同步资源未获得改资源,而使得较高优先级进程/线程反而推迟被调度执行的现象。根据阻塞类型的不同,优先级反转又被分为Bounded priority inversionUnbounded priority inversion

优先级反转的后果

  • 低优先级的任务比高优先级的任务先执行,导致任务的错乱,逻辑错乱;
  • 可能造成系统崩溃;
  • 死锁;优先级低的线程迟迟得不到调度,具有高优先级的线程不能执行,死锁;
Bounded priority inversion

高优先级任务(Task H)被持有锁的低优先级任务(Task L)阻塞,由于阻塞的时间取决于低优先级任务在临界区的时间(持有锁的时间),所以被称为bounded priority inversion。只要Task L一直持有锁,Task H就会一直被阻塞,低优先级的任务运行在高优先级任务的前面,优先级被反转。

这里的任务也可以理解为线程

图片

// PriorityInversionDemo.m
#import <Foundation/Foundation.h>
#import <pthread.h>
#import <unistd.h>pthread_mutex_t mutex;void *lowPriorityTask(void *arg) {NSLog(@"[L] 低优先级任务启动,准备加锁");pthread_mutex_lock(&mutex);NSLog(@"[L] 低优先级任务获得锁,进入临界区");sleep(3); // 模拟长时间占用临界区NSLog(@"[L] 低优先级任务即将释放锁");pthread_mutex_unlock(&mutex);NSLog(@"[L] 低优先级任务结束");return NULL;
}void *mediumPriorityTask(void *arg) {sleep(1); // 确保低优先级先获得锁NSLog(@"[M] 中优先级任务启动,开始忙等");for (int i = 0; i < 5; i++) {NSLog(@"[M] 中优先级任务运行中...");usleep(500 * 1000); // 0.5秒}NSLog(@"[M] 中优先级任务结束");return NULL;
}void *highPriorityTask(void *arg) {sleep(2); // 确保低优先级先获得锁NSLog(@"[H] 高优先级任务启动,准备加锁");pthread_mutex_lock(&mutex);NSLog(@"[H] 高优先级任务获得锁,进入临界区");pthread_mutex_unlock(&mutex);NSLog(@"[H] 高优先级任务结束");return NULL;
}int main(int argc, const char * argv[]) {@autoreleasepool {pthread_mutex_init(&mutex, NULL);pthread_t low, medium, high;// 创建低优先级线程pthread_create(&low, NULL, lowPriorityTask, NULL);// 创建中优先级线程pthread_create(&medium, NULL, mediumPriorityTask, NULL);// 创建高优先级线程pthread_create(&high, NULL, highPriorityTask, NULL);// 等待线程结束pthread_join(low, NULL);pthread_join(medium, NULL);pthread_join(high, NULL);pthread_mutex_destroy(&mutex);}return 0;
}

请添加图片描述

上述代码用 pthread_mutex_t 互斥锁演示了 bounded priority inversion。低优先级线程持有锁,高优先级线程被阻塞,而中优先级线程频繁运行导致低优先级线程无法及时释放锁,形成优先级反转。只要低优先级线程能运行并释放锁,反转就会结束,这就是“有界”的含义。

Unbounded priority inversion

Task L持有锁的情况下,如果有一个中间优先级的任务(Task M)打断了Task L,前面的bounded就会变为unbounded,因为Task M只要抢占了Task LCPU,就可能会阻塞Task H任意多的时间(Task M可能不止1个)。

图片

我们写个demo来验证:

// UnboundedPriorityInversionDemo.m
#import <Foundation/Foundation.h>
#import <pthread.h>
#import <unistd.h>pthread_mutex_t mutex;void *lowPriorityTask(void *arg) {NSLog(@"[L] 低优先级任务启动,准备加锁");pthread_mutex_lock(&mutex);NSLog(@"[L] 低优先级任务获得锁,进入临界区");sleep(2); // 模拟临界区操作NSLog(@"[L] 低优先级任务即将释放锁");pthread_mutex_unlock(&mutex);NSLog(@"[L] 低优先级任务结束");return NULL;
}void *mediumPriorityTask(void *arg) {sleep(1); // 确保低优先级先获得锁NSLog(@"[M] 中优先级任务启动,开始长时间运行");for (int i = 0; i < 10; i++) {NSLog(@"[M] 中优先级任务运行中...%d", i);usleep(500 * 1000); // 0.5秒}NSLog(@"[M] 中优先级任务结束");return NULL;
}void *highPriorityTask(void *arg) {sleep(1); // 确保低优先级先获得锁NSLog(@"[H] 高优先级任务启动,准备加锁");pthread_mutex_lock(&mutex);NSLog(@"[H] 高优先级任务获得锁,进入临界区");pthread_mutex_unlock(&mutex);NSLog(@"[H] 高优先级任务结束");return NULL;
}int main(int argc, const char * argv[]) {@autoreleasepool {pthread_mutex_init(&mutex, NULL);pthread_t low, medium, high;// 创建低优先级线程pthread_create(&low, NULL, lowPriorityTask, NULL);// 创建高优先级线程pthread_create(&high, NULL, highPriorityTask, NULL);// 创建中优先级线程pthread_create(&medium, NULL, mediumPriorityTask, NULL);// 等待线程结束pthread_join(low, NULL);pthread_join(medium, NULL);pthread_join(high, NULL);pthread_mutex_destroy(&mutex);}return 0;
}

按照预想中的逻辑:

  • 低优先级线程 L 先获得锁,进入临界区。

  • 高优先级线程 H 启动后,尝试加锁,被阻塞。

  • 中优先级线程 M 启动后,不断运行,占用 CPU,导致 L 得不到运行机会,H 也无法获得锁。

  • 只要 M 持续运行,H 就会被无限期阻塞,形成unbounded priority inversion。

我们实际跑一下:

请添加图片描述

发现并不是,中优先级并不能一直抢占线程。这是因为:

  • 普通用户态线程(如 macOS 下的 pthread)默认不会严格抢占,优先级只是一个“建议”,不是强制的。

  • 线程的实际调度还受到系统负载、CPU 核心数、线程创建顺序等影响。

解决方法

目前解决Unbounded priority inversion2种方法:一种被称作优先权极限(priority ceiling protocol),另一种被称作优先级继承(priority inheritance)。

优先权极限(priority ceiling protocol

系统把每一个临界资源与 1 个极限优先权相关联。当1个任务进入临界区时,系统便把这个极限优先权传递给这个任务,使得这个任务的优先权最高;当这个任务退出临界区后,系统立即把它的优先权恢复正常,从而保证系统不会出现优先权反转的情况。该极限优先权的值是由所有需要该临界资源的任务的最大优先级来决定的。

如图所示,锁的极限优先权是 3。当Task L持有锁的时候,它的优先级将会被提升到3,和Task H一样的优先级。这样就可以阻止Task M(优先级是2)的运行,直到Task LTask H不再需要该锁。

在这里插入图片描述

优先级继承(priority inheritance

大致原理是:高优先级任务在尝试获取锁的时候,如果该锁正好被低优先级任务持有,此时会临时把高优先级线程的优先级转移给拥有锁的低优先级线程,使低优先级线程能更快的执行并释放同步资源,释放同步资源后再恢复其原来的优先级。

图片

priority ceiling protocolpriority inheritance都会在释放锁的时候,恢复低优先级任务的优先级。同时要注意,以上2种方法只能阻止Unbounded priority inversion,而无法阻止Bounded priority inversionTask H必须等待Task L执行完毕才能执行,这个反转是无法避免的)。

这里还需要注意一点:优先级继承必须是可以传递的。比如说:当T1阻塞在被T2持有的资源上,而T2又阻塞在T3持有的一个资源上。如果T1的优先级高于T2T3的优先级,T3必须通过T2继承T1的优先级。否则,如果另外一个优先级高于T2T3,小于T1的线程T4,将抢占T3,引发相对于T1的优先级反转。因此,线程所继承的优先级必须是直接或者间接阻塞的线程的最高优先级。

面试题

1. 什么是优先级反转?请解释其概念并举例说明。

答:优先级反转是指一个低优先级的任务持有了一个共享资源的锁,而高优先级的任务等待该资源释放,此时中优先级的任务运行并抢占了低优先级的CPU先执行了,会导致高优先级的任务被“反转”到中优先级任务之后运行。

举例:

假设有三个任务A、B、C,它们的优先级分别为高、中、低。任务C持有一个锁并正在运行,任务A需要该锁才能运行,但任务B抢占了CPU时间。这会导致,原本任务A优先级更高,应该在任务B前执行,却导致任务A必须等待任务B完成后才能运行。

2. 在iOS开发中,如何避免或解决优先级反转问题?

  1. 优先级继承 : 确保持有锁的低优先级任务提升到高优先级任务的级别,直到释放锁,再恢复成原来的优先级。(记录了锁持有者的api都可以自动避免优先级反转,系统会通过提高相关线程的优先级来解决优先级反转的问题,如 dispatch_sync)
  2. 避免使用dispatch_semphore(信号量)做线程同步:dispatch_semaphore 容易造成优先级反转,因为api没有记录是哪个线程持有了信号量,所以有高优先级的线程在等待锁的时候,内核无法知道该提高那个线程的优先级(QoS);
  3. 使用合适的锁机制:选择合适的锁机制(如NSLock、NSRecursiveLock等)和避免长时间持有锁。
  4. 避免锁竞争:减少共享资源的使用和锁的粒度,避免长时间锁竞争。

这里解释一下第二点:

如果当前线程因等待某线程(线程 1)上正在进行的操作(如 block1)而受阻,而系统知道 block1 所在的目标线程(owner),系统会通过提高相关线程的优先级来解决优先级反转的问题。反之如果系统不知道 block1 所在目标线程,则无法知道应该提高谁的优先级,也就无法解决反转问题;

记录了持有者信息(owner)的系统 API 如下:

  1. pthread mutexos_unfair_lock、以及基于这二者实现的上层API

    a. dispatch_once 的实现是基于 os_unfair_lock

    b. NSLockNSRecursiveLock@synchronized 等的实现是基于 pthreadmutex

  2. dispatch_syncdispatch_wait

  3. xpc_connection_send_with_message_sync

使用以上API即可

http://www.dtcms.com/a/321140.html

相关文章:

  • 解决Docker部署的MySQL8错误日志里面的 mbind: Operation not permitted 问题
  • 构建安全 Web 应用:从用户认证与授权到 JWT 原理解析
  • python使用python-docx自动化操作word
  • 【杂谈】-逆缩放悖论:为何更多思考会让AI变“笨“?
  • Numpy科学计算与数据分析:Numpy布尔索引与花式索引实战
  • 一种对白点进行多重加权并利用三角剖分插值微调白平衡增益的白平衡矫正算法
  • RAG问答系统:Spring Boot + ChromaDB 知识库检索实战
  • 3D Tiles 格式中,Bounding Volum
  • 基于AutoDL平台的3D_Gaussian_Splatting初体验
  • 在 Vue 中使用 ReconnectingWebSocket实现即时通讯聊天客服功能
  • 2025 前端真实试题-阿里面试题分析
  • 关于数据结构6-哈希表和5种排序算法
  • Maptalks vs Cesium
  • 【最新版】2025年搜狗拼音输入法
  • “电子合同”为什么会出现无效的情况?
  • OpenCV cv2.flip() 函数详解与示例
  • 深入理解 Java AWT Container:原理、实战与性能优化
  • ORACLE看当前连接数的方法
  • 柠檬笔试——野猪骑士
  • 南方略咨询与与清源科技正式启动国际市场GTM流程规划咨询项目!!!
  • 汽车电子:现代汽车的“神经中枢“
  • Eyevinn 彻底改变开源部署模式
  • 小孙学变频学习笔记(十三)电动机参数的自动测量 矢量控制的转速反馈
  • 如何 让ubuntu 在root 下安装的docker 在 普通用户下也能用
  • Spring Boot 结合 CORS 解决前端跨域问题
  • GitLab同步提交的用户设置
  • 2025年渗透测试面试题总结-08(题目+回答)
  • 【19】C#实战篇—— C# 绘制点划线,绘制虚线——PointF dxdy,过x点垂直画红色点划线,长度为W,过y点水平画红色点划线,长度为H
  • 华清远见25072班C语言学习day5
  • 自动驾驶数据闭环