erInitiated;
// 模块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
可以发现,通过手动调整其优先级,低优先级任务的整体耗时得到大幅度的降低,这样在持有锁的情况下,可以减少对主线程的阻塞时间。
上线效果
该问题的验证过程分为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;
深刻理解优先级反转
那么是否所有锁都需要像上文一样,手动提升持有锁的线程优先级?系统是否会自动调整线程的优先级?如果有这样的机制,是否可以覆盖所有的锁?要理解这些问题,需要深刻认识优先级反转。
什么是优先级反转?
优先级反转,是指某同步资源被较低优先级的进程/线程所拥有,较高优先级的进程/线程竞争该同步资源未获得该资源,而使得较高优先级进程/线程反而推迟被调度执行的现象。根据阻塞类型的不同,优先级反转又被分为Bounded priority inversion
和Unbounded priority inversion
。这里借助 Introduction to RTOS - Solution to Part 11 (Priority Inversion) 的图进行示意。
Bounded priority inversion
如图所示,高优先级任务(Task H
)被持有锁的低优先级任务(Task L
)阻塞,由于阻塞的时间取决于低优先级任务在临界区的时间(持有锁的时间),所以被称为bounded priority inversion
。只要Task L
一直持有锁,Task H
就会一直被阻塞,低优先级的任务运行在高优先级任务的前面,优先级被反转。
这里的任务也可以理解为线程
Unbounded priority inversion
在Task L
持有锁的情况下,如果有一个中间优先级的任务(Task M
)打断了Task L
,前面的bounded
就会变为unbounded
,因为Task M
只要抢占了Task L
的CPU
,就可能会阻塞Task H
任意多的时间(Task M
可能不止1
个)
优先级反转常规解决思路
目前解决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
执行完毕才能执行,这个反转是无法避免的)。
可以通过以下几种发生来避免或者转移Bounded priority inversion
:
- 减少临界区的执行时间,减少
Bounded priority inversion
的反转耗时;
- 避免使用会阻塞高优先级任务的临界区资源;
- 专门使用一个队列来管理资源,避免使用锁。
优先级继承必须是可传递的。举个栗子:当T1
阻塞在被T2
持有的资源上,而T2
又阻塞在T3
持有的一个资源上。如果T1
的优先级高于T2
和T3
的优先级,T3
必须通过T2
继承T1
的优先级。否则,如果另