全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2022-11-14_ART线程同步实现

您的位置:首页 >> 新闻 >> 行业资讯

ART线程同步实现 本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究! 引言我们在Andoird性能优化 - 死锁监控与其背后的小知识这篇中提到过死锁检测的方法,其中我们涉及到两个native层中提供给我们重要方法,分别是查看当前线程持有的“锁”的GetContendedMonitor方法 ObjPtrmirror::ObjectMonitor::GetContendedMonitor(Thread*thread){ //ThisisusedtoimplementJDWP'sThreadReference.CurrentContendedMonitor,andhasabizarre //definitionofcontendedthatincludesamonitorathreadistryingtoenter... ObjPtrmirror::Objectresult=thread-GetMonitorEnterObject(); if(result==nullptr){ //...butalsoamonitorthatthethreadiswaitingon. MutexLockmu(Thread::Current(),*thread-GetWaitMutex()); Monitor*monitor=thread-GetWaitMonitor(); if(monitor!=nullptr){ result=monitor-GetObject(); } } returnresult; } 还有就是获取当前“锁”所在的Thread的GetLockOwnerThreadId方法 uint32_tMonitor::GetLockOwnerThreadId(ObjPtrmirror::Objectobj){ DCHECK(obj!=nullptr); LockWordlock_word=obj-GetLockWord(true); switch(lock_word.GetState()){ caseLockWord::kHashCode: //Fall-through. caseLockWord::kUnlocked: returnThreadList::kInvalidThreadId; caseLockWord::kThinLocked: returnlock_word.ThinLockOwner(); caseLockWord::kFatLocked:{ Monitor*mon=lock_word.FatLockMonitor(); returnmon-GetOwnerThreadId(); } default:{ LOG(FATAL)"Unreachable"; UNREACHABLE(); } } } 我们所说的“锁”,都是以java层的视角去解释的,同时我们模拟死锁的操作是用synchronized这个java层的关键字去模拟的。那么肯定有读者有疑问,为什么native中虚拟机会提供出来这么几个方法?同时我们可以留意到,在上面两个方法中,涉及到了Monitor,LockWord,mirror::Object等几个类,它们之间存在什么关系呢?为什么获取“锁”跟通过“锁”获取Thread id的操作都涉及到了它们呢? 本篇将从native层的角度,去探究java层的“锁”的真正实现 线程同步我们在java层常用的线程同步手段,synchronized可谓是一个非常热门的用法,因为它是java/kotlin的关键字,天生就带有线程同步的能力。我们举个例子,就是用synchronized去修饰代码块的场景 synchronized(xx){ 同步逻辑 } 其实这段代码,就是在进入代码段的时候,生成了一条MONITOR_ENTER指令,而离开代码段的时候,生成一条MONITOR_EXIT指令,当然,我们并不那么关心指令的真正在不同架构的实现,我们关注能生成指令的native虚拟机上层代码,所以接下来我们就来介绍一下Monitor,LockWord,mirror::Object这几个新成员了,它们之间的调用关系如图 在上面三个类中,真正负责提供同步机制的,其实“只有”Monitor这个类,它是Art虚拟机中的包装类 Monitor根据上图,我们可以看到Monitor中含有一个叫monitor_lock的成员变量,类型是Mutex,我们也知道,虚拟机本身它是没有线程同步的功能的,因此真正同步的肯定还是要依靠操作系统本身的实现,操作系统提供的不同机制有很多,比如linux中的futex,比如pthread_mutex_t等,选择哪种方式是根据当前系统架构决定的,比如可以通过ART_USE_FUTEXES宏判断同步机制是否由futex提供。 art/runtime/base/mutex.h #ifdefined(__linux__) #defineART_USE_FUTEXES1 #else #defineART_USE_FUTEXES0 #endif 这里我们点到为止就好,真正的同步机制根据操作系统不同实现细节肯定是不同的,我们这里还是以认识Monitor类为主。 monitor_id是一个MonitorId的类型,其实就是说每一个Monitor对象都有个id,作为Monitor类的唯一标识,类型是32位的无符号整型 obj_,每一个Monitor都会关联一个Object对象 当然,Monitor还有很多个对象,这里就不再一一举例,可以在art/runtime/monitor.h中看到Monitor的定义。Monitor本身还提供了Lock,UnLock等线程同步方法去给我们程序使用。 Object这里说的Object可不是我们java层的Object.java类,而是指native层的Object类(接下来我们说的Object都是native层的Object),它有一个名为monitor_的成员变量,但是!!注意这里,虽然Monitor类的obj_指向了一个Object对象,但是这里的Object类的monitor_却并不是Monitor,而是指向了一个LockWord对象。(为什么说是指向,因为存的类型是一个指针) LockWordLockWord,它只有一个uint32类型的成员变量,可以说这是一个非常“简洁”的类,从上面我们了解到,Onject类中的monitor_指向的是一个LockWord对象,那么为什么要指向这么一个对象呢?我们来回顾一下,synchronized是不是可以修饰一个对象或者类本身,去实现一个同步操作。那么为什么无论是类还是对象都能用synchronized修饰呢?其实就是因为java层的类或者对象都可以是native的Object的一个体现,而我们Object里面都有monitor_对象。但是这也我们思考一下,并不是所有的java层对象或者类都能被synchronized修饰对不对,我们再回顾一下Monitor,我们说了Monitor背后其实还是靠着操作系统的同步机制去现实线程同步,而这个操作系统同步操作,本来就是一个非常重量级的资源因此对于Object来说,它没有直接与Monitor关联起来,而是以LockWord作为纽带,在需要的时候我们才进行Monitor关联,从而达到资源最大化的效果。所以LockWord,本身可以算是一个中间层,也可以说是一个分发器,由它进行是否上锁操作。 LockWord源码在这,我们可以看到,他定义了几种状态 enumLockState{ kUnlocked,//Nolockowners.未上锁 kThinLocked,//Singleuncontendedowner.ThinLock,瘦锁 kFatLocked,//Seeassociatedmonitor.FatLock胖锁 这两个跟锁没有太大关系,我们忽略 kHashCode,//Lockwordcontainsanidentityhash. kForwardingAddress,//Lockwordcontainstheforwardingaddressofanobject. }; 我们看到LockState的定义,其实就能够明白了,LockWord通过LockState,指明了当前Object处于一个怎样的同步状态。比如如果当前是kUnlocked状态,那么就不用去浪费Monitor的资源,如果是kFatLocked状态,那么我们就需要去“绑定”Monitor了。那么LockWork怎么判断当前属于哪种情况呢?回顾一下上面的类图,其实就是通过成员变量value(32位)标识实现的 image.pngvalue的30-31位用来描述锁的形态,我们只关心锁的部分,00 代表KStateThinOrunlocked(对应着锁的无锁跟瘦锁状态),01 为kStateFat (胖锁) KStateThinOrunlocked形态时,28-29代表一个ReadBarrier(读屏障),16-27代表一个计数器(记录该锁被调用的次数,比如线程递归调用),0-15位时持有锁线程的id kStateFat形态时,28-29代表一个ReadBarrier(读屏障),0-27位代表一个Monitor对象的monitor_id 我们猜猜看,为什么虚拟机要设计这么一个一个结构,我们从我们熟悉的synchronized去入手 synchronized native实现我们面试的时候肯定背过synchronized,比如锁升级流程呀,偏向锁转变为轻量级然后重量级这么一个过程,可能大部分我们都只停留在了面经的阶段,下面我们来看一下,为什么会有这么几个阶段,同时synchronized是怎么实现的。 回到开头,我们说过synchronized会在代码块开始前插入MONITOR_ENTER指令,在结束时插入MONITOR_EXIT指令,这里我们并不关心真正的操作系统指令,而是上一层的虚拟机代码生成,这两条分别对应着MonitorEnter 方法和MonitorExit art/runtime/mirror/object-inl.h inlineObjPtrmirror::ObjectObject::MonitorEnter(Thread*self){ returnMonitor::MonitorEnter(self,this,/*trylock=*/false); } inlineboolObject::MonitorExit(Thread*self){ returnMonitor::MonitorExit(self,this); } 看到这里,接下来就是硬核的扒源码环节 MonitorEnter ObjPtrmirror::ObjectMonitor::MonitorEnter(Thread*self, ... while(true){ 把接下来保存的内容放在LockWord中 LockWordlock_word=h_obj-GetLockWord(false); switch(lock_word.GetState()){ 如果是未上锁状态,则生成一个状态时ThinLock的LockWord,当前次数是0 caseLockWord::kUnlocked:{ //Noorderingrequiredforprecedinglockwordread,sinceweretest. LockWordthin_locked(LockWord::FromThinLockId(thread_id,0,lock_word.GCState())); if(h_obj-CasLockWord(lock_word,thin_locked,CASMode::kWeak,std::memory_order_acquire)){ AtraceMonitorLock(self,h_obj.Get(),/*is_wait=*/false); returnh_obj.Get();//Success! } continue;//Goagain. } 如果当前是瘦锁,先判断当前拥有锁的线程跟当前调用线程是否是同一个,如果是同一个线程,则增加ThinLockCount caseLockWord::kThinLocked:{ uint32_towner_thread_id=lock_word.ThinLockOwner(); if(owner_thread_id==thread_id){ //Noorderingrequiredforinitiallockwordread. //Weownthelock,increasetherecursioncount. uint32_tnew_count=lock_word.ThinLockCount()+ 这里有个细节,虽然是同一个线程如果超出kThinLockMaxCount,则通过InflateThinLocked变成胖锁 if(LIKELY(new_count=LockWord::kThinLockMaxCount)){ LockWordthin_locked(LockWord::FromThinLockId(thread_id, new_count, lock_word.GCState())); //Onlythisthreadpaysattentiontothecount.Thusthereisnoneedforstronger //thanrelaxedmemoryordering. if(!gUseReadBarrier){ h_obj-SetLockWord(thin_locked,/*as_volatile=*/false); AtraceMonitorLock(self,h_obj.Get(),/*is_wait=*/false); returnh_obj.Get();//Success! }else{ //UseCAStopreservethereadbarrierstate. if(h_obj-CasLockWord(lock_word, thin_locked, CASMode::kWeak, std::memory_order_relaxed)){ AtraceMonitorLock(self,h_obj.Get(),/*is_wait=*/false); returnh_obj.Get();//Success! } } continue;//Goagain. }else{ //We'doverflowtherecursioncount,soinflatethemonitor. InflateThinLocked(self,h_obj,lock_word, } }else{ 如果调用线程跟当前锁线程不是同一个,则增加contention_count if(trylock){ returnnullptr; } //Contention. contention_count++; Runtime*runtime=Runtime::Current(); 如果contention_count if(contention_count=kExtraSpinIters+runtime-GetMaxSpinsBeforeThinLockInflation()时且contention_countkExtraSpinIters,则主动让出当前cpu调度(这里我们可以思考一下为什么) =kExtraSpinIters+runtime-GetMaxSpinsBeforeThinLockInflation()){ if(contention_countkExtraSpinIters){ sched_yield(); } }else{ contention_count= //Noorderingrequiredforinitiallockwordread.Installrereadsitanyway. InflateThinLocked(self,h_obj,lock_word, } } continue;//Startfromthebeginning. }, 如果是胖锁通过Monitor,对象mon-Lock(self);未持有锁时,会一直阻塞,lock之后就是成功获取到锁的逻辑 caseLockWord::kFatLocked:{ std::atomic_thread_fence(std::memory_order_acquire); Monitor*mon=lock_word.FatLockMonitor(); if(trylock){ returnmon-TryLock(self)?h_obj.Get():nullptr; }else{ mon-Lock(self); DCHECK(mon-monitor_lock_.IsExclusiveHeld(self)); returnh_obj.Get();//Success! } ... 通过上面对MonitorEnter的分析,我们能够明白了它的核心,就是通过当前LockWord的状态去进行LockWord对应的升级。为了描述简单,我们接下来把LockWord称为“锁”,但是希望读者能够明白跟Monitor的区别,我们这里简单总结一下流程,主要分为了两个大分支,一个是当前调用线程跟锁的拥有线程是同一个时与不同时的区分。 如果当前请求锁的线程跟锁的拥有线程是同一个,那么就看当前LockWord的状态,如果是无锁状态,则生成一个状态处于瘦锁的LockWord,我们再来回顾一下LockWord对应状态的不同状态 可以看到ThinState瘦锁状态有个lockcount的概念,就是锁的调用次数。从无锁变成瘦锁时,瘦锁初始化时lockcount就是0。如果当前是瘦锁,如果lockcount超过了LockWord::kThinLockMaxCount,4096,那么就要通过InflateThinLocked变成胖锁。如果是胖锁,就通过Lock方法直接获取(trylock为false),Lock方法会阻塞当前线程直到获取到锁 如果当前请求锁的线程跟锁的拥有线程是不同的,未上锁跟胖锁的状态处理基本一致,对于当前处于瘦锁的时候,有个小优化点,就是if (contention_count = kExtraSpinIters + runtime-GetMaxSpinsBeforeThinLockInflation() 时且contention_count kExtraSpinIters,则主动让出当前cpu调度,这是因为线程让出cpu到线程重新获取cpu的时候,有一定的时间差,可能持有锁的线程已经释放了锁,这个时候就不需要阻塞了,直接获取效率更高。这算是art虚拟机的一个优化点,但是这个优化点还在一直迭代中,至少androd10 - android13已经更新好多个版本了,因为这个优化点有个弱点就是,多核cpu情况下,可能当前cpu让出调用,在其他的核中又立马获取到调度了,因此存在无用功的可能性比较大(无效调度),因此android13 时多了kExtraSpinIters这个纬度去判断是否让出线程调度的方案。 瘦锁升级为胖锁我们在MonitorEnter过程中,能够看到在瘦锁处理过程中,会通过InflateThinLocked方法变成胖锁,我们来了解一下这个升级过程,InflateThinLocked内部会调用Inflate方法进行真正的升级,inflate函数就是要将输入参数obj包含的LockWord对象跟Monitor绑定起来 voidMonitor::Inflate(Thread*self,Thread*owner,ObjPtrmirror::Objectobj,int32_thash_code){ DCHECK(self!=nullptr); DCHECK(obj!=nullptr); //Allocateandacquireanewmonitor. Monitor*m=MonitorPool::CreateMonitor(self,owner,obj,hash_code); DCHECK(m!=nullptr); if(m-Install(self)){ if(owner!=nullptr){ VLOG(monitor)"monitor:thread"owner-GetThreadId() "createdmonitor"m"forobject"obj; }else{ VLOG(monitor)"monitor:Inflatewithhashcode"hash_code "createdmonitor"m"forobject"obj; } Runtime::Current()-GetMonitorList()-Add(m); CHECK_EQ(obj-GetLockWord(true).GetState(),LockWord::kFatLocked); }else{ MonitorPool::ReleaseMonitor(self, } } inflate方法会调用Install方法,在这里我们关注一下瘦锁的升级 boolMonitor::Install(Thread*self)NO_THREAD_SAFETY_ANALYSIS{ Thread*owner=owner_.load(std::memory_order_relaxed); CHECK(owner==nullptr||owner==self||owner-IsSuspended()); //Propagatethelockstate. LockWordlw(GetObject()-GetLockWord(false)); switch(lw.GetState()){ caseLockWord::kThinLocked:{ DCHECK(owner!=nullptr); CHECK_EQ(owner-GetThreadId(),lw.ThinLockOwner()); DCHECK_EQ(monitor_lock_.GetExclusiveOwnerTid(),0)"mytid="SafeGetTid(self); 保存瘦锁的信息 lock_count_=lw.ThinLockCount(); monitor_lock_.ExclusiveLockUncontendedFor(owner); DCHECK_EQ(monitor_lock_.GetExclusiveOwnerTid(),owner-GetTid()) "mytid="SafeGetTid(self); 构造了一个胖锁对象,通过CasLockWord中cas操作把这个胖锁对象替换原来的瘦锁 LockWordfat(this,lw.GCState()); //Publishtheupdatedlockword,whichmayracewithotherthreads. boolsuccess=GetObject()-CasLockWord(lw,fat,CASMode::kWeak,std::memory_order_release); if(success){ if(ATraceEnabled()){ SetLockingMethod(owner); } returntrue; }else{ monitor_lock_.ExclusiveUnlockUncontended(); returnfalse; } } inflate过程还是比较直观的,通过inflate方法把瘦锁变成了胖锁的过程,其实是构建了一个新的LockWord对象,并设置状态为胖锁,后续通过cas操作把这个Object的LockWord对象进行替换 MonitorExitboolMonitor::MonitorExit(Thread*self,ObjPtrmirror::Objectobj){ ... while(true){ LockWordlock_word=obj-GetLockWord(true); switch(lock_word.GetState()){ //异常case处理,没有上锁或者锁状态不对 caseLockWord::kHashCode: //Fall-through. caseLockWord::kUnlocked: FailedUnlock(h_obj.Get(),self-GetThreadId(),0u,nullptr); returnfalse;//Failure. //瘦锁释放 caseLockWord::kThinLocked:{ uint32_tthread_id=self-GetThreadId(); uint32_towner_thread_id=lock_word.ThinLockOwner(); 如果当前持有锁的线程跟想要释放锁的线程不是同一个,这也是一种错误 if(owner_thread_id!=thread_id){ FailedUnlock(h_obj.Get(),thread_id,owner_thread_id,nullptr); returnfalse;//Failure. }else{ //Weownthelock,decreasetherecursioncount. LockWordnew_lw=LockWord::Default(); 瘦锁释放就是直接让ThinLockCount-1 if(lock_word.ThinLockCount()!=0){ uint32_tnew_count=lock_word.ThinLockCount; new_lw=LockWord::FromThinLockId(thread_id,new_count,lock_word.GCState()); }else{ new_lw=LockWord::FromDefault(lock_word.GCState()); } if(!gUseReadBarrier){ DCHECK_EQ(new_lw.ReadBarrierState(), h_obj-SetLockWord(new_lw,true); AtraceMonitorUnlock(); //Success! returntrue; }else{ //UseCAStopreservethereadbarrierstate. if(h_obj-CasLockWord(lock_word,new_lw,CASMode::kWeak,std::memory_order_release)){ AtraceMonitorUnlock(); //Success! returntrue; } } continue;//Goagain. } } 胖锁就是直接调用Unlock方法 caseLockWord::kFatLocked:{ Monitor*mon=lock_word.FatLockMonitor(); returnmon-Unlock(self); } default:{ LOG(FATAL)"Invalidmonitorstate"lock_word.GetState(); UNREACHABLE(); } } } } MonitorExit比较简单,就是直接按照分配逻辑进行释放,首先判断了当前锁状态是否正确,比如kUnlocked状态就不能释放,瘦锁就是lockcount-1即可,胖锁则调用Unlock方法。 总结通过对native层的线程同步的理解,我们明白了Monitor,LockWord,Object之间的关系,也就能明白虚拟机侧提供的各种获取锁关系的api的内部为什么会这么实现了。 阅读原文

上一篇:2024-08-11_「转」数百万晶体数据训练、解决晶体学相位问题,深度学习方法PhAI登Science 下一篇:2022-08-18_30项运动的剪辑,太丝滑了!

TAG标签:

16
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价