「iOS」————优先级反转
iOS学习
- 优先级反转问题
- 优先级反转的后果
- Bounded priority inversion
- Unbounded priority inversion
- 解决方法
- 面试题
优先级反转问题
什么是优先级反转?
优先级反转是指:某同步资源被较低优先级的进程/线程所拥有,较高优先级的线程/进程竞争改同步资源未获得改资源,而使得较高优先级进程/线程反而推迟被调度执行的现象。根据阻塞类型的不同,优先级反转又被分为Bounded priority inversion
和Unbounded 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 L
的CPU
,就可能会阻塞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 inversion
有2
种方法:一种被称作优先权极限(priority ceiling protocol
),另一种被称作优先级继承(priority inheritance
)。
优先权极限(priority ceiling protocol
)
系统把每一个临界资源与 1 个极限优先权相关联。当1个任务进入临界区时,系统便把这个极限优先权传递给这个任务,使得这个任务的优先权最高;当这个任务退出临界区后,系统立即把它的优先权恢复正常,从而保证系统不会出现优先权反转的情况。该极限优先权的值是由所有需要该临界资源的任务的最大优先级来决定的。
如图所示,锁的极限优先权是 3。当Task L
持有锁的时候,它的优先级将会被提升到3,和Task H
一样的优先级。这样就可以阻止Task M
(优先级是2)的运行,直到Task L
和Task H
不再需要该锁。
优先级继承(priority inheritance
)
大致原理是:高优先级任务在尝试获取锁的时候,如果该锁正好被低优先级任务持有,此时会临时把高优先级线程的优先级转移给拥有锁的低优先级线程,使低优先级线程能更快的执行并释放同步资源,释放同步资源后再恢复其原来的优先级。
priority ceiling protocol
和priority inheritance
都会在释放锁的时候,恢复低优先级任务的优先级。同时要注意,以上2
种方法只能阻止Unbounded priority inversion
,而无法阻止Bounded priority inversion
(Task H
必须等待Task L
执行完毕才能执行,这个反转是无法避免的)。
这里还需要注意一点:优先级继承必须是可以传递的。比如说:当T1
阻塞在被T2
持有的资源上,而T2
又阻塞在T3
持有的一个资源上。如果T1
的优先级高于T2
和T3
的优先级,T3
必须通过T2
继承T1
的优先级。否则,如果另外一个优先级高于T2
和T3
,小于T1
的线程T4
,将抢占T3
,引发相对于T1
的优先级反转。因此,线程所继承的优先级必须是直接或者间接阻塞的线程的最高优先级。
面试题
1. 什么是优先级反转?请解释其概念并举例说明。
答:优先级反转是指一个低优先级的任务持有了一个共享资源的锁,而高优先级的任务等待该资源释放,此时中优先级的任务运行并抢占了低优先级的CPU先执行了,会导致高优先级的任务被“反转”到中优先级任务之后运行。
举例:
假设有三个任务A、B、C,它们的优先级分别为高、中、低。任务C持有一个锁并正在运行,任务A需要该锁才能运行,但任务B抢占了CPU时间。这会导致,原本任务A优先级更高,应该在任务B前执行,却导致任务A必须等待任务B完成后才能运行。
2. 在iOS开发中,如何避免或解决优先级反转问题?
- 优先级继承 : 确保持有锁的低优先级任务提升到高优先级任务的级别,直到释放锁,再恢复成原来的优先级。(记录了锁持有者的api都可以自动避免优先级反转,系统会通过提高相关线程的优先级来解决优先级反转的问题,如 dispatch_sync)
- 避免使用dispatch_semphore(信号量)做线程同步:dispatch_semaphore 容易造成优先级反转,因为api没有记录是哪个线程持有了信号量,所以有高优先级的线程在等待锁的时候,内核无法知道该提高那个线程的优先级(QoS);
- 使用合适的锁机制:选择合适的锁机制(如NSLock、NSRecursiveLock等)和避免长时间持有锁。
- 避免锁竞争:减少共享资源的使用和锁的粒度,避免长时间锁竞争。
这里解释一下第二点:
如果当前线程因等待某线程(线程 1)上正在进行的操作(如 block1
)而受阻,而系统知道 block1
所在的目标线程(owner
),系统会通过提高相关线程的优先级来解决优先级反转的问题。反之如果系统不知道 block1
所在目标线程,则无法知道应该提高谁的优先级,也就无法解决反转问题;
记录了持有者信息(owner
)的系统 API 如下:
-
pthread mutex
、os_unfair_lock
、以及基于这二者实现的上层APIa.
dispatch_once
的实现是基于os_unfair_lock
的b.
NSLock
、NSRecursiveLock
、@synchronized
等的实现是基于pthreadmutex
-
dispatch_sync
、dispatch_wait
-
xpc_connection_send_with_message_sync
使用以上API即可