1. 从一个线上问题说起

最近在线上遇到了一些[HMDConfigManager remoteConfigWithAppID:]卡死

1.1 开端剖析

调查了下主线程仓库,用到的锁是读写锁:

优先级反转那些事儿

随后又去翻了下持有着锁的子线程,有各种各样的状况,且基本都处于正常的履行状况,例如有的处于翻开文件状况,有的处于read状况,有的正在履行NSUserDefaults的办法

优先级反转那些事儿

优先级反转那些事儿

优先级反转那些事儿

经过调查发现,出问题的线程都有`QOS:BACKGROUND`符号。全体看起来持有锁的子线程仍然在履行,只是留给主线程的时刻不够了。为什么这些子线程在持有锁的状况下,需求履行这么久,直到主线程的 8s 卡死?一种状况就是真的如此耗时,另一种则是出现了优先级回转。

1.2 处理办法

这个案例里,持有读写锁且优先级低的线程迟迟得不到调度(又或许得到调度的时分又被抢占了,或许得到调度的时分时刻已然不够了)而具有高优先级的线程因为拿不到读写锁,一向被堵塞,所以互相死锁。iOS8之后引入了QualityOfService的概念,类似于线程的优先级,设置不同的QualityOfService的值后体系会分配不同的CPU时刻、网络资源和硬盘资源等,因而咱们能够经过这个设置行列的优先级。

1.2.1计划一:去除对 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.

苹果的建议是不要随意修正线程的优先级,尤其是这些凹凸优先级线程之间存在临界资源竞赛的状况。所以删去相关优先级设置代码即可处理问题。

1.2.2 计划二:暂时修正线程优先级

在 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 sectio n.

尽管针对的是实时体系,可是还是有一些启示和协助。依照提示,对有问题的代码进行了修正:在线程经过pthread_rwlock_wrlock拿到 _rwlock的时分,暂时进步其优先级,在开释 _rwlock之后,康复其原先的优先级。

-(id)remoteConfigWithAppID:(NSString*)appID
{
.......
pthread_rwlock_rdlock(&_rwlock);
HMDHeimdallrConfig*result=.......//getexistingconfig
pthread_rwlock_unlock(&_rwlock);

if(result==nil){
result=[[HMDHeimdallrConfigalloc]init];//makeanewconfig
pthread_rwlock_wrlock(&_rwlock);

qos_class_toldQos=qos_class_self();
BOOLneedRecover=NO;

//暂时进步线程优先级
if(_enablePriorityInversionProtection&&oldQos<QOS_CLASS_USER_INTERACTIVE){
intret=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);
}
}

returnresult;
}

值得注意的是,这儿只能运用pthreadapiNSThread供给的API是不可行的

1.3 Demo 验证

为了验证上述的手动调整线程优先级是否有一定的作用,这儿经过demo进行本地实验:界说了2000operation(意图是为了CPU繁忙),优先级设置NSQualityOfServiceUserInitiated,且对其中能够被100整除的operation的优先级调整为NSQualityOfServiceBackground,在每个operation履行相同的耗时使命,然后对这被选中的10operation进行耗时计算。

for(intj=0;j<2000;++j){
NSOperationQueue*operation=[[NSOperationQueuealloc]init];
operation.maxConcurrentOperationCount=1;
operation.qualityOfService=NSQualityOfServiceUserInitiated;

//模块1
//if(j%100==0){
//operation.qualityOfService=NSQualityOfServiceBackground;
//}
//模块1

[operationaddOperationWithBlock:^{
//模块2
//qos_class_toldQos=qos_class_self();
//pthread_set_qos_class_self_np(QOS_CLASS_USER_INITIATED,0);
//模块2

NSTimeIntervalstart=CFAbsoluteTimeGetCurrent();
doublesum=0;
for(inti=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
}];
}

计算信息如下表所示:

优先级反转那些事儿

能够看到:

  1. 正常状况下,每个使命的平均耗时为:11.8190561;
  2. operation被设置为低优先级时,其耗时大幅度进步为:94.70210189;
  3. operation被设置为低优先级时,又在Block中手动康复其原有的优先级,其耗时现已大幅度下降:15.04005137(耗时比正常状况高,大家能够考虑下为什么)

经过Demo能够发现,经过手动调整其优先级,低优先级使命的全体耗时得到大幅度的下降,这样在持有锁的状况下,能够削减对主线程的堵塞时刻。

1.4 上线作用

优先级反转那些事儿
该问题的验证过程分为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

    a. QOS_CLASS_BACKGROUNDMach层级优先级数是4;

    b. QOS_CLASS_DEFAULTMach层级优先级数是31;

    c. QOS_CLASS_USER_INITIATEDMach层级优先级数是37。

所以相对来说,线上的进步相对有限。

2. 深刻了解优先级回转

那么是否一切锁都需求像上文相同,手动进步持有锁的线程优先级?体系是否会自动调整线程的优先级?假如有这样的机制,是否能够覆盖一切的锁?要了解这些问题,需求深刻认识优先级回转。

2.1 什么是优先级回转?

优先级回转,是指某同步资源被较低优先级的进程/线程所具有,较高优先级的进程/线程竞赛该同步资源未获得该资源,而使得较高优先级进程/线程反而推延被调度履行的现象。依据堵塞类型的不同,优先级回转又被分为Bounded priority inversionUnbounded priority inversion

这儿凭借 Introduction to RTOS – Solution to Part 11 的图进行暗示。

2.1.1 Bounded priority inversion

如图所示,高优先级使命(Task H)被持有锁的低优先级使命(Task L)堵塞,因为堵塞的时刻取决于低优先级使命在临界区的时刻(持有锁的时刻),所以被称为bounded priority inversion。只需Task L一向持有锁,Task H就会一向被堵塞,低优先级的使命运转在高优先级使命的前面,优先级被回转。

这儿的使命也能够了解为线程

优先级反转那些事儿

2.1.2 Unbounded priority inversion

Task L持有锁的状况下,假如有一个中间优先级的使命(Task M)打断了Task L,前面的bounded就会变为unbounded,因为Task M只需抢占了Task LCPU,就可能会堵塞Task H恣意多的时刻(Task M可能不止1个)。

优先级反转那些事儿

2.2 优先级回转常规处理思路

现在处理Unbounded priority inversion2种办法:一种被称作优先权极限(priority ceiling protocol),另一种被称作优先级承继(priority inheritance)。

2.2.1 Priority ceiling protocol

在优先权极限计划中,体系把每一个临界资源与 1 个极限优先权相关联。当1个使命进入临界区时,体系便把这个极限优先权传递给这个使命,使得这个使命的优先权最高;当这个使命退出临界区后,体系立即把它的优先权康复正常,然后保证体系不会出现优先权回转的状况。该极限优先权的值是由一切需求该临界资源的使命的最大优先级来决议的。

如图所示,锁的极限优先权是 3。当Task L持有锁的时分,它的优先级将会被进步到3,和Task H相同的优先级。这样就能够阻挠Task M(优先级是2)的运转,直到Task LTask H不再需求该锁。

优先级反转那些事儿

2.2.2 Priority inheritance

在优先级承继计划中,大致原理是:高优先级使命在尝试获取锁的时分,假如该锁正好被低优先级使命持有,此时会暂时把高优先级线程的优先级搬运给具有锁的低优先级线程,使低优先级线程能更快的履行并开释同步资源,开释同步资源后再康复其原来的优先级。

优先级反转那些事儿

priority ceiling protocolpriority inheritance都会在开释锁的时分,康复低优先级使命的优先级。一起要注意,以上2种办法只能阻挠Unbounded priority inversion,而无法阻挠Bounded priority inversionTask H必须等候Task L履行结束才能履行,这个回转是无法防止的)。

能够经过以下几种发生来防止或许搬运Bounded priority inversion

  1. 削减临界区的履行时刻,削减Bounded priority inversion的回转耗时;
  1. 防止运用会堵塞高优先级使命的临界区资源;
  1. 专门运用一个行列来管理资源,防止运用锁。

优先级承继必须是可传递的。举个栗子:当T1堵塞在被T2持有的资源上,而T2又堵塞在T3持有的一个资源上。假如T1的优先级高于T2T3的优先级,T3必须经过T2承继T1的优先级。不然,假如别的一个优先级高于T2T3,小于T1的线程T4,将抢占T3,引发相对于T1的优先级回转。因而,线程所承继的优先级必须是直接或许直接堵塞的线程的最高优先级。

3. 怎么防止优先级回转?

3.1 QoS 传递

iOS 体系首要运用以下两种机制来在不同线程(或queue)间传递QoS

  • 机制 1:dispatch_async
    • dispatch_async()automatically propagates the QoS from the calling thread, though it will translate User Interactive to User Initiated to avoid assigning that priority to non-main threads.
    • Capturedattimeofblocksubmission,translateuserinteractivetouserinitiated.UsedifdestinationqueuedoesnothaveaQoSanddoesnotlowertheQoS(exdispatch_asyncbacktothemainthread).
  • 机制 2:根据 XPC 的进程间通讯(IPC
    • 体系的 QoS 传递规矩比较复杂,首要参阅以下信息:

      • 其时线程的QoS
      • 假如是运用dispatch_block_create() 办法生成的dispatch_block,则考虑生成block时所调用的参数
      • dispatch_asyncIPC的方针queue或线程的QoS

调度程序会依据这些信息决议block以什么优先级运转。

  1. 假如没有其他线程同步地等候此block,则block就按上面所说的优先级来运转。
  2. 假如出现了线程间同步等候的状况,则调度程序会依据状况调整线程的运转优先级。

3.2 怎么触发优先级回转防止机制?

假如其时线程因等候某线程(线程 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能够在发生优先级回转时使体系启用优先级回转防止机制。

3.3 基础 API 验证

接下来对前文说到的各种「基础体系API」进行验证

测试验证环境:模拟器 iOS15.2

3.3.1 pthread mutex

pthread mutex的数据结构pthread_mutex_s其中有一个m_tid字段,专门来记载持有该锁的线程Id

//types_internal.h
structpthread_mutex_s{
longsig;
_pthread_locklock;
union{
uint32_tvalue;
structpthread_mutex_options_soptions;
}mtxopts;
int16_tprioceiling;
int16_tpriority;
#ifdefined(__LP64__)
uint32_t_pad;
#endif
union{
struct{
uint32_tm_tid[2];//threadidofthreadthathasmutexlocked
uint32_tm_seq[2];//mutexsequenceid
uint32_tm_mis[2];//formisalignedlocksm_tid/m_seqwillspan intohere
}psynch;
struct_pthread_mutex_ulock_sulock;
};
#ifdefined(__LP64__)
uint32_t_reserved[4];
#else
uint32_t_reserved[1];
#endif
};

代码来验证一下:线程优先级是否会被进步?

//printThreadPriority用来打印线程的优先级信息
voidprintThreadPriority(){
thread_tcur_thread=mach_thread_self();
mach_port_deallocate(mach_task_self(),cur_thread);
mach_msg_type_number_tthread_info_count=THREAD_INFO_MAX;
thread_info_data_tthinfo;
kern_return_tkr=thread_info(cur_thread,THREAD_EXTENDED_INFO,(thread_info_t)thinfo,&thread_info_count);
if(kr!=KERN_SUCCESS){
return;
}
thread_extended_info_textend_info=(thread_extended_info_t)thinfo;
printf("pth_priority:%d,pth_curpri:%d,pth_maxpriority:%d\n",extend_info->pth_priority,extend_info->pth_curpri,extend_info->pth_maxpriority);
}

先在子线程上锁并休眠,然后主线程恳求该锁。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0),^{
printf("begin:\n");
printThreadPriority();
printf("queuebeforelock\n");
pthread_mutex_lock(&_lock);//保证backgroundQueue先得到锁
printf("queuelock\n");
printThreadPriority();
dispatch_async(dispatch_get_main_queue(),^{
printf("beforemainlock\n");
pthread_mutex_lock(&_lock);
printf("inmainlock\n");
pthread_mutex_unlock(&_lock);
printf("aftermainunlock\n");
});
sleep(10);
printThreadPriority();
printf("queueunlock\n");
pthread_mutex_unlock(&_lock);
printf("queueafterunlock\n");
});
begin:
pth_priority:4,pth_curpri:4,pth_maxpriority:63
queuebeforelock
queuelock
pth_priority:4,pth_curpri:4,pth_maxpriority:63
beforemainlock
pth_priority:47,pth_curpri:47,pth_maxpriority:63
queueunlock
inmainlock
aftermainunlock
queueafterunlock

能够看到,低优先级子线程先持有锁,其时的优先级为4,而该锁被主线程恳求的时分,子线程的优先级被进步为47

3.3.2 os_unfair_lock

os_unfair_lock用来替换OSSpinLock,处理优先级回转问题。等候os_unfair_lock锁的线程会处于休眠状况,从用户态切换到内核态,而并非忙等。os_unfair_lock将线程ID保存到了锁的内部,锁的等候者会把自己的优先级让出来,然后防止优先级回转。验证一下:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0),^{
printf("begin:\n");
printThreadPriority();
printf("queuebeforelock\n");
os_unfair_lock_lock(&_unfair_lock);//保证backgroundQueue先得到锁
printf("queuelock\n");
printThreadPriority();
dispatch_async(dispatch_get_main_queue(),^{
printf("beforemainlock\n");
os_unfair_lock_lock(&_unfair_lock);
printf("inmainlock\n");
os_unfair_lock_unlock(&_unfair_lock);
printf("aftermainunlock\n");
});
sleep(10);
printThreadPriority();
printf("queueunlock\n");
os_unfair_lock_unlock(&_unfair_lock);
printf("queueafterunlock\n");
});
begin:
pth_priority:4,pth_curpri:4,pth_maxpriority:63
queuebeforelock
queuelock
pth_priority:4,pth_curpri:4,pth_maxpriority:63
beforemainlock
pth_priority:47,pth_curpri:47,pth_maxpriority:63
queueunlock
inmainlock
aftermainunlock
queueafterunlock

成果和pthread mutex共同。

3.3.3 pthread_rwlock_t

在 pthread_rwlock_init 有如下提示:

Caveats : Beware ofpriority inversionwhen using read-write locks. A high-priority thread may be blocked waiting on a read-write lock locked by a low-priority thread. The microkernel has no knowledge of read-write locks, and therefore can’t boost the low-priority thread to prevent the priority inversion.

粗心是内核不感知读写锁,无法进步低优先级线程的优先级,然后无法防止优先级回转。经过查询界说发现:pthread_rwlock_s包含了字段rw_tid,专门来记载持有写锁的线程,这不由令人猎奇:为什么pthread_rwlock_sowner信息却仍然无法防止优先级回转?

structpthread_rwlock_s{
longsig;
_pthread_locklock;
uint32_t
unused:29,
misalign:1,
pshared:2;
uint32_trw_flags;
#ifdefined(__LP64__)
uint32_t_pad;
#endif
uint32_trw_tid[2];//threadidofthreadthathasexclusive(write)lock
uint32_trw_seq[4];//rwsequenceid(at128-bitalignedboundary)
uint32_trw_mis[4];//formisalignedlocksrw_seqwillspan intohere
#ifdefined(__LP64__)
uint32_t_reserved[34];
#else
uint32_t_reserved[18];
#endif
};

news.ycombinator.com/item?id=217… 链接中说到:

xnu supports priority inheritance through “turnstiles“, a kernel-internal mechanism which is used by default by a number of locking primitives (list at [1]), including normal pthread mutexes (though not read-write locks [2]), as well as the os_unfair_lock API (via the ulock syscalls). With pthread mutexes, you can actually explicitly request priority inheritance by calling pthread_mutexattr_setprotocol [3] with PTHREAD_PRIO_INHERIT; the Apple implementation supports it, but currently ignores the protocol setting and just gives all mutexes priority inheritance.

粗心是:XNU 运用 turnstiles内核机制进行优先级承继,这种机制被应用在pthread mutexos_unfair_lock上。

顺藤摸瓜,在ksyn_wait办法中找到了_kwq_use_turnstile的调用,其中的注释对读写锁解释的比较委婉,添加了at least sometimes

pthread mutexes andrwlocks both (at least sometimes) know their owner and can use turnstiles. Otherwise, we pass NULL as the tstore to the shims so they wait on the global waitq.

//libpthread/kern/kern_synch.c
int
ksyn_wait(ksyn_wait_queue_tkwq,kwq_queue_type_tkqi,uint32_tlockseq,
intfit,uint64_tabstime,uint16_tkwe_flags,
thread_continue_tcontinuation,block_hint_tblock_hint)
{
thread_tth=current_thread();
uthread_tuth=pthread_kern->get_bsdthread_info(th);
structturnstile**tstore=NULL;
intres;
assert(continuation!=THREAD_CONTINUE_NULL);
ksyn_waitq_element_tkwe=pthread_kern->uthread_get_uukwe(uth);
bzero(kwe,sizeof(*kwe));
kwe->kwe_count=1;
kwe->kwe_lockseq=lockseq&PTHRW_COUNT_MASK;
kwe->kwe_state=KWE_THREAD_INWAIT;
kwe->kwe_uth=uth;
kwe->kwe_thread=th;
kwe->kwe_flags=kwe_flags;
res=ksyn_queue_insert(kwq,kqi,kwe,lockseq,fit);
if(res!=0){
//panic("psynch_rw_wrlock:failedtoenqueue\n");//XXXksyn_wqunlock(kwq);
returnres;
}
PTHREAD_TRACE(psynch_mutex_kwqwait,kwq->kw_addr,kwq->kw_inqueue,
kwq->kw_prepost.count,kwq->kw_intr.count);
if(_kwq_use_turnstile(kwq)){
//pthreadmutexesandrwlocksboth(atleastsometimes)knowtheir
//ownerandcanuseturnstiles.Otherwise,wepassNULLasthe
//tstoretotheshimssotheywaitontheglobalwaitq.
tstore=&kwq->kw_turnstile;
}
......
}

再去查看_kwq_use_turnstile的界说,代码还是很诚实的,只要在KSYN_WQTYPE_MTX才会启用turnstile进行优先级回转保护,而读写锁的类型为KSYN_WQTYPE_RWLOCK,这说明读写锁不会运用_kwq_use_turnstile,所以无法防止优先级回转。

#defineKSYN_WQTYPE_MTX0x01
#defineKSYN_WQTYPE_CVAR0x02
#defineKSYN_WQTYPE_RWLOCK0x04
#defineKSYN_WQTYPE_SEMA0x08
staticinlinebool
_kwq_use_turnstile(ksyn_wait_queue_tkwq)
{
//Ifwehadwriter-ownerinformationfromthe
//rwlockthenwecouldusetheturnstiletopushonit.Fornow,only
//plainmutexesuseit.
return(_kwq_type(kwq)==KSYN_WQTYPE_MTX);
}

别的在_pthread_find_owner也能够看到,读写锁的owner0

void
_pthread_find_owner(thread_tthread,
structstackshot_thread_waitinfo*waitinfo)
{
ksyn_wait_queue_tkwq=_pthread_get_thread_kwq(thread);
switch(waitinfo->wait_type){
casekThreadWaitPThreadMutex:
assert((kwq->kw_type&KSYN_WQTYPE_MASK)==KSYN_WQTYPE_MTX);
waitinfo->owner=thread_tid(kwq->kw_owner);
waitinfo->context=kwq->kw_addr;
break;
/*Ownerofrwlocknotstoredinkernelspaceduetoraces.Punt
*andhopethattheuserspaceaddressishelpfulenough.*/
casekThreadWaitPThreadRWLockRead:
casekThreadWaitPThreadRWLockWrite:
assert((kwq->kw_type&KSYN_WQTYPE_MASK)==KSYN_WQTYPE_RWLOCK);
waitinfo->owner=0;
waitinfo->context=kwq->kw_addr;
break;
/*Condvarsdon'thaveowners,sojustgivetheuserspaceaddress.*/
casekThreadWaitPThreadCondVar:
assert((kwq->kw_type&KSYN_WQTYPE_MASK)==KSYN_WQTYPE_CVAR);
waitinfo->owner=0;
waitinfo->context=kwq->kw_addr;
break;
casekThreadWaitNone:
default:
waitinfo->owner=0;
waitinfo->context=0;
break;
}
}

把锁更换为读写锁,验证一下前面的理论是否正确:

pthread_rwlock_init(&_rwlock,NULL);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0),^{
printf("begin:\n");
printThreadPriority();
printf("queuebeforelock\n");
pthread_rwlock_rdlock(&_rwlock);//保证backgroundQueue先得到锁
printf("queuelock\n");
printThreadPriority();
dispatch_async(dispatch_get_main_queue(),^{
printf("beforemainlock\n");
pthread_rwlock_wrlock(&_rwlock);
printf("inmainlock\n");
pthread_rwlock_unlock(&_rwlock);
printf("aftermainunlock\n");
});
sleep(10);
printThreadPriority();
printf("queueunlock\n");
pthread_rwlock_unlock(&_rwlock);
printf("queueafterunlock\n");
});
begin:
pth_priority:4,pth_curpri:4,pth_maxpriority:63
queuebeforelock
queuelock
pth_priority:4,pth_curpri:4,pth_maxpriority:63
beforemainlock
pth_priority:4,pth_curpri:4,pth_maxpriority:63
queueunlock
queueafterunlock
inmainlock
aftermainunlock

能够看到读写锁不会发生优先级进步。

3.3.4 dispatch_sync

这个API都比较熟悉了,这儿直接验证:

//其时线程为主线程
dispatch_queue_attr_tqosAttribute=dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL,QOS_CLASS_BACKGROUND,0);
_queue=dispatch_queue_create("com.demo.test",qosAttribute);
printThreadPriority();
dispatch_async(_queue,^{
printf("dispatch_asyncbeforedispatch_sync:\n");
printThreadPriority();
});
dispatch_sync(_queue,^{
printf("dispatch_sync:\n");
printThreadPriority();
});
dispatch_async(_queue,^{
printf("dispatch_asyncafterdispatch_sync:\n");
printThreadPriority();
});
pth_priority:47,pth_curpri:47,pth_maxpriority:63
dispatch_asyncbeforedispatch_sync:
pth_priority:47,pth_curpri:47,pth_maxpriority:63
dispatch_sync:
pth_priority:47,pth_curpri:47,pth_maxpriority:63
dispatch_asyncafterdispatch_sync:
pth_priority:4,pth_curpri:4,pth_maxpriority:63

_queue是一个低优先级行列(QOS_CLASS_BACKGROUND),能够看到dispatch_sync调用压入行列的使命,以及在这之前dispatch_async压入的使命,都被进步到较高的优先级47(和主线程共同),而最终一个dispatch_async的使命则以优先级4来履行。

3.3.5 dispatch_wait

//其时线程为主线程
dispatch_queue_attr_tqosAttribute=dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL,QOS_CLASS_BACKGROUND,0);
_queue=dispatch_queue_create("com.demo.test",qosAttribute);
printf("mainthread\n");
printThreadPriority();
dispatch_block_tblock=dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS,^{
printf("subthread\n");
sleep(2);
printThreadPriority();
});
dispatch_async(_queue,block);
dispatch_wait(block,DISPATCH_TIME_FOREVER);

_queue是一个低优先级行列(QOS_CLASS_BACKGROUND),当在其时主线程运用dispatch_wait进行等候时,输出如下,低优先级的使命被进步到优先级47

mainthread
pth_priority:47,pth_curpri:47,pth_maxpriority:63
subthread
pth_priority:47,pth_curpri:47,pth_maxpriority:63

而假如将dispatch_wait(block, DISPATCH_TIME_FOREVER)注释掉之后,输出如下:

mainthread
pth_priority:47,pth_curpri:47,pth_maxpriority:63
subthread
pth_priority:4,pth_curpri:4,pth_maxpriority:63

值得注意的是,dispatch_wait是一个宏(C11的泛型),或许是一个入口函数,它能够承受dispatch_block_tdispatch_group_tdispatch_semaphore_t3种类型的参数,可是这儿的具体含义应该是指dispatch_block_wait,只要dispatch_block_wait会调整优先级,防止优先级回转。

intptr_t
dispatch_wait(void*object,dispatch_time_ttimeout);
#if__has_extension(c_generic_selections)
#definedispatch_wait(object,timeout)\
_Generic((object),\
dispatch_block_t:dispatch_block_wait,\
dispatch_group_t:dispatch_group_wait,\
dispatch_semaphore_t:dispatch_semaphore_wait\
)((object),(timeout))
#endif

3.4 奥秘的信号量

3.4.1 dispatch_semaphore

之前对dispatch_semaphore的认知十分浅薄,经常把二值信号量和互斥锁划等号。可是经过调研后发现:dispatch_semaphore没有QoS的概念,没有记载其时持有信号量的线程(owner),所以有高优先级的线程在等候锁时,内核无法知道该进步哪个线程的调试优先级(QoS)。假如锁持有者优先级比其他线程低,高优先级的等候线程将一向等候。Mutexvs Semaphore: What’s the Difference? 一文具体比对了MutexSemaphore之间的区别。

Semaphores are for signaling (sames a condition variables, events) while mutexes are for mutual exclusion.Technically, you can also use semaphores for mutual exclusion (a mutex can be thought as a binary semaphore) but you really shouldn’t.

Right, but libdispatch doesn’t have a mutex. It has semaphores and queues.So if you’re trying to use libdispatch and you don’t want the closure-based aspect of queues, you might be tempted to use a semaphore instead. Don’t do that, use os_unfair_lock or pthread_mutex(or a higher-level construct like NSLock) instead.

这些是一些警示,能够看到dispatch_semaphore十分风险,运用需求特别小心。

这儿经过苹果官方供给的demo进行解释:

__blockNSString*taskName=nil;
dispatch_semaphore_tsema=dispatch_semaphore_create(0);
[self.connection.remoteObjectProxyrequestCurrentTaskName:^(NSString*task){
taskName=task;
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema,DISPATCH_TIME_FOREVER);
returntaskName;
  1. 假设在主线程履行这段代码,那么其时线程的优先级是QOS_CLASS_USER_INTERACTIVE
  2. 因为从主线程进行了异步,异步使命行列的QoS将会被进步为QOS_CLASS_USER_INITIATED
  3. 主线程被信号量sema堵塞,而担任开释该信号量的异步使命的优先级QOS_CLASS_USER_INITIATED低于主线程的优先级QOS_CLASS_USER_INTERACTIVE,因而可能会发生优先级回转。

值得一提的是,Clang专门针对这种状况进行了静态检测:github.com/llvm-mirror…

staticautofindGCDAntiPatternWithSemaphore()->decltype(compoundStmt()){
constchar*SemaphoreBinding="semaphore_name";
autoSemaphoreCreateM=callExpr(allOf(
callsName("dispatch_semaphore_create"),
hasArgument(0,ignoringParenCasts(integerLiteral(equals(0))))));
autoSemaphoreBindingM=anyOf(
forEachDescendant(
varDecl(hasDescendant(SemaphoreCreateM)).bind(SemaphoreBinding)),
forEachDescendant(binaryOperator(bindAssignmentToDecl(SemaphoreBinding),
hasRHS(SemaphoreCreateM))));
autoHasBlockArgumentM=hasAnyArgument(hasType(
hasCanonicalType(blockPointerType())
));
autoArgCallsSignalM=hasAnyArgument(stmt(hasDescendant(callExpr(
allOf(
callsName("dispatch_semaphore_signal"),
equalsBoundArgDecl(0,SemaphoreBinding)
)))));
autoHasBlockAndCallsSignalM=allOf(HasBlockArgumentM,ArgCallsSignalM);
autoHasBlockCallingSignalM=
forEachDescendant(
stmt(anyOf(
callExpr(HasBlockAndCallsSignalM),
objcMessageExpr(HasBlockAndCallsSignalM)
)));
autoSemaphoreWaitM=forEachDescendant(
callExpr(
allOf(
callsName("dispatch_semaphore_wait"),
equalsBoundArgDecl(0,SemaphoreBinding)
)
).bind(WarnAtNode));
returncompoundStmt(
SemaphoreBindingM,HasBlockCallingSignalM,SemaphoreWaitM);
}

假如想运用该功用,只需求翻开xcode设置即可:

别的,dispatch_groupsemaphore类似,在调用enter()办法时,无法预知谁会调用leave(),所以体系也无法知道其owner是谁,所以同样不会有优先级进步的问题。

3.4.2 信号量卡死言传身教

dispatch_semaphore给笔者的印象十分深刻,之前写过一段这样的代码:运用信号量在主线程同步等候相机授权成果。

__blockBOOLauth=NO;
dispatch_semaphore_tsemaphore=dispatch_semaphore_create(0);
[KTAuthorizeServicerequestAuthorizationWithType:KTPermissionsTypeCameracompletionHandler:^(BOOLallow){
auth=allow;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);

上线后长时间占有卡死top1,其时百思不得其解,在深化了解到信号量无法防止优先级回转后,终于豁然开朗,一扫之前心中的阴霾。

优先级反转那些事儿

这类问题一般经过2种方式来处理:

  1. 运用同步API
BOOLauth=[KTAuthorizeServiceauthorizationWithType:KTPermissionsTypeCamera];
//dosomethingnext
  1. 异步回调,不要在其时线程等候
[KTAuthorizeServicerequestAuthorizationWithType:KTPermissionsTypeCameracompletionHandler:^(BOOLallow){
BOOLauth=allow;
//dosomethingnextviacallback
}];

4. 几个概念

4.1 turnstile

前文说到XNU运用turnstile进行优先级承继,这儿对turnstile机制进行简略的描绘和了解。在XNU内核中,存在着很多的同步目标(例如lck_mtx_t),为了处理优先级回转的问题,每个同步目标都必须对应一个分离的数据结构来保护很多的信息,例如堵塞在这个同步目标上的线程行列。能够幻想一下,假如每个同步目标都要分配一个这样的数据结构,将形成极大的内存糟蹋。

为了处理这个问题,XNU采用了turnstile机制,一种空间利用率很高的处理计划。该计划的提出依据是同一个线程在同一时刻不能一起堵塞于多个同步目标上。这一事实允许一切同步目标只需求保留一个指向turnstile的指针,且在需求的时分去分配一个turnstile即可,而turnstile则包含了操作一个同步目标需求的一切信息,例如堵塞线程的行列、具有这个同步目标的线程指针。turnstile是从池中动态分配的,这个池的大小会随着体系中已分配的线程数目添加而添加,所以turnstile总数将始终低于或等于线程数,这也决议了turnstile的数目是可控的。turnstile由堵塞在该同步目标上的第一个线程担任分配,当没有更多线程堵塞在该同步目标上,turnstile会被开释,回收到池中。

turnstile的数据结构如下:

structturnstile{
structwaitqts_waitq;/*waitqembeddedinturnstile*/
turnstile_inheritor_tts_inheritor;/*thread/turnstileinheritingthepriority(IL,WL)*/
union{
structturnstile_listts_free_turnstiles;/*turnstilefreelist(IL)*/
SLIST_ENTRY(turnstile)ts_free_elm;/*turnstilefreelistelement(IL)*/
};
structpriority_queue_sched_maxts_inheritor_queue;/*Queueofturnstilewithusasaninheritor(WL)*/
union{
structpriority_queue_entry_schedts_inheritor_links;/*Inheritorqueuelinks*/
structmpsc_queue_chaints_deallocate_link;/*threaddeallocatelink*/
};
SLIST_ENTRY(turnstile)ts_htable_link;/*linkageforturnstileinglobalhashtable*/
uintptr_tts_proprietor;/*hashkeylookupturnstile(IL)*/
os_refcnt_tts_refcount;/*referencecountforturnstiles*/
_Atomicuint32_tts_type_gencount;/*gencountusedforprioritychaining(IL),typeofturnstile(IL)*/
uint32_tts_port_ref;/*numberofexplicitrefsfromportsonsendturnstile*/
turnstile_update_flags_tts_inheritor_flags;/*flagsforturnstileinheritor(IL,WL)*/
uint8_tts_priority;/*priorityofturnstile(WL)*/
#ifDEVELOPMENT||DEBUG
uint8_tts_state;/*currentstateofturnstile(IL)*/
queue_chain_tts_global_elm;/*globalturnstilechain*/
thread_tts_thread;/*threadtheturnstileisattachedto*/
thread_tts_prev_thread;/*threadtheturnstilewasattachedbeforedonation*/
#endif
};

4.2 优先级数值

在验证环节有一些优先级数值,这儿凭借「Mac OS X and iOS Internals」解释一下:实验中涉及到的优先级数值都是相对于Mach层而言的,且都是用户线程数值。

  1. 用户线程的优先级是0~63;

    a. NSQualityOfServiceBackgroundMach层级优先级数是4;

    b. NSQualityOfServiceUtility的Mach层级优先级数是20;

    c. NSQualityOfServiceDefaultMach层级优先级数是31;

    d. NSQualityOfServiceUserInitiated的Mach层级优先级数是37;

    e. NSQualityOfServiceUserInteractive的Mach层级优先级是47。

  2. 内核线程的优先级是80~95;

  3. 实时体系线程的优先级是96~127;

  4. 64~79被保留给体系运用。

优先级反转那些事儿

5. 总结

本文首要阐述了优先级回转的一些概念和处理思路,并结合iOS平台的几种锁进行了具体的调研。经过深化的了解,能够去躲避一些不必要的优先级回转,然后进一步防止卡死反常。字节跳动APM团队也针对线程的优先级做了监控处理,进而达到发现和防备优先级回转的意图。

6. 参阅文档

  1. WWDC18 What’ s New in LLVM – actorsfit
  1. developer.apple.com/videos/play…
  1. developer.apple.com/forums/thre…
  1. developer.apple.com/library/arc…
  1. developer.apple.com/library/arc…
  1. github.com/llvm-mirror…
  1. Don’t use dispatch semaphores where mutexes (or dispatch queues) would suffice
  1. Concurrency Problems Written by Scott Grosch
  1. www.jianshu.com/p/af64e05de…
  1. pubs.opengroup.org/onlinepubs/…
  1. iOS中各种“锁”的了解及应用
  1. 不再安全的 OSSpinLock
  1. blog.actorsfit.com/a?ID=00001-…
  1. objccn.io/issue-2-1/#…
  1. Introduction to RTOS – Solution to Part 11 (Priority Inversion)
  1. threadreaderapp.com/thread/1229…
  1. 深化了解iOS中的锁
  1. Threads can infect each other with their low priority

7. 参加咱们

字节跳动APM中台致力于进步整个集团内全系产品的功能和稳定性体现,技术栈覆盖 iOS / Android / Server / Web / Hybrid / PC / 游戏 / 小程序等,工作内容包含但不限于功能稳定性监控,问题排查,深度优化,防劣化等。长时间希望为业界输出更多更有建设性的问题发现和深度优化手段。

欢迎对字节 APM 团队职位感兴趣的同学投递简历到邮箱xushuangqing@bytedance.com

优先级反转那些事儿

添加小助手回复【APM】可参加功能监控沟通群,获取更多技术干货