溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

多線程之線程同步

發(fā)布時(shí)間:2020-07-11 13:33:44 來源:網(wǎng)絡(luò) 閱讀:229 作者:張立達(dá) 欄目:網(wǎng)絡(luò)安全

多線程內(nèi)容大致分兩部分,其一是異步操作,可通過專用,線程池,Task,Parallel,PLINQ等,而這里又涉及工作線程與IO線程;其二是線程同步問題,鄙人現(xiàn)在學(xué)習(xí)與探究的是線程同步問題。

通過學(xué)習(xí)《CLR via C#》里面的內(nèi)容,對線程同步形成了脈絡(luò)較清晰的體系結(jié)構(gòu),在多線程中實(shí)現(xiàn)線程同步的是線程同步構(gòu)造,這個(gè)構(gòu)造分兩大類,一個(gè)是基元構(gòu)造,一個(gè)是混合構(gòu)造。所謂基元?jiǎng)t是在代碼中使用最簡單的構(gòu)造?;獦?gòu)造又分成兩類,一個(gè)是用戶模式,另一個(gè)是內(nèi)核模式。而混合構(gòu)造則是在內(nèi)部會使用基元構(gòu)造的用戶模式和內(nèi)核模式,使用它的模式會有一定的策略,因?yàn)橛脩裟J胶蛢?nèi)核模式各有利弊,混合構(gòu)造則是為了平衡兩者的利與弊而設(shè)計(jì)出來。下面則列舉整個(gè)線程同步體系結(jié)構(gòu)

  1. 基元

    1.1 用戶模式

    1.1.1 volatile

    1.1.2 Interlock

    1.2 內(nèi)核模式

    1.2.1 WaitHandle

    1.2.2 ManualResetEvent與AutoResetEvent

    1.2.3 Semaphore

    1.2.4 Mutex

  2. 混合

    2.1 各種Slim

    2.2 Monitor

    2.3 MethodImplAttribute與SynchronizationAttribute

    2.4 ReaderWriterLock

    2.5 Barier(少用)

    2.6 CoutdownEvent(少用)

   

先從線程同步問題的原因說起,當(dāng)內(nèi)存中有一個(gè)×××的變量A,里面存放的值是2,當(dāng)線程1執(zhí)行的時(shí)候它會把A的值從內(nèi)存中取出存放到CPU的寄存器中,并把A賦值為3,此時(shí)剛好線程1的時(shí)間片結(jié)束;接著CPU把時(shí)間片分給線程2,線程2同樣把A從內(nèi)存中的值取出來放到內(nèi)存中,但是由于線程1并沒有把變量A的新值3放回內(nèi)存,故線程2讀到的仍然是舊的值(也就是臟數(shù)據(jù))2,然后線程2要是需要對A值進(jìn)行一些判斷之類的就會出現(xiàn)一些非預(yù)期的結(jié)果了。

而針對上面這種對資源的共享問題處理,往往會使用各種各樣辦法。下面則逐一介紹

   

先說說基元構(gòu)造中的用戶模式,凡是用戶模式的優(yōu)點(diǎn)是它的執(zhí)行相對較快,因?yàn)樗峭ㄟ^一系列CPU指令來協(xié)調(diào),它造成的阻塞只是極短時(shí)間的阻塞,對操作系統(tǒng)而言這個(gè)線程是一直在運(yùn)行,從未被阻塞。缺點(diǎn)就是唯有系統(tǒng)內(nèi)核才能停止這樣的一個(gè)線程運(yùn)行。另一方面就是由于線程在自旋而非阻塞,那么它還會占用這CPU的時(shí)間,造成對CPU時(shí)間的浪費(fèi)。

首先是基元用戶模式構(gòu)造中的volatile構(gòu)造,這個(gè)構(gòu)造網(wǎng)上很多說法是讓CPU對指定字段(Field,也就是變量)的讀都是從內(nèi)存讀,每次寫都是往內(nèi)存寫。然而它和編譯器的代碼優(yōu)化有關(guān)系。先看看如下代碼

多線程之線程同步

    public class StrageClass
    {
        volatile int mFlag = 0;        int mValue = 0; 
        public void Thread1()
        {
            mValue = 5;
            mFlag = 1;
        } 
        public void Thread2()
        {            if (mFlag == 1)
                Console.WriteLine(mValue);
        }
    }

多線程之線程同步

 

在懂得多線程同步問題的同學(xué)們都會知道如果用兩個(gè)線程分別去執(zhí)行上面兩個(gè)方法時(shí),得出的結(jié)果有兩個(gè):1.不輸出任何東西;2.輸出5。但是在CSC編譯器編譯成IL語言或JIT編譯成機(jī)器語言的過程中,會進(jìn)行代碼優(yōu)化,在方法Thread1中,編譯器會覺得給兩個(gè)字段賦值會沒什么所謂,它只會站在單個(gè)線程執(zhí)行的角度來看,完全不會顧及多線程的問題,因此它有可能會把兩行代碼的執(zhí)行順序調(diào)亂,導(dǎo)致先給mFlag賦值為1,再給mValue賦值為5,這就導(dǎo)致了第三種結(jié)果,輸出0??上н@種結(jié)果我一直無法測試出來。

解決這個(gè)現(xiàn)象的就是volatile構(gòu)造,使用了這種構(gòu)造的效果是,凡是對使用了此構(gòu)造的字段進(jìn)行讀操作時(shí),該操作都保證在原有代碼順序下會在最先執(zhí)行;或者是凡是對使用了此構(gòu)造的字段進(jìn)行寫操作時(shí),該操作都保證在原有代碼順序下會在最后執(zhí)行。

實(shí)現(xiàn)了volatile的構(gòu)造現(xiàn)在來說有三個(gè),其一是Thread的兩個(gè)靜態(tài)方法VolatileRead和VolatileWrite,在MSND上的解析如下

Thread.VolatileRead 讀取字段值。 無論處理器的數(shù)目或處理器緩存的狀態(tài)如何,該值都是由計(jì)算機(jī)的任何處理器寫入的最新值。

Thread.VolatileWrite 立即向字段寫入一個(gè)值,以使該值對計(jì)算機(jī)中的所有處理器都可見。

在多處理器系統(tǒng)上, VolatileRead 獲得由任何處理器寫入的內(nèi)存位置的最新值。 這可能需要刷新處理器緩存;VolatileWrite 確保寫入內(nèi)存位置的值立即可見的所有處理器。 這可能需要刷新處理器緩存。

即使在單處理器系統(tǒng)上, VolatileRead 和 VolatileWrite 確保值為讀取或?qū)懭雰?nèi)存,并不緩存 (例如,在處理器寄存器中)。 因此,您可以使用它們可以由另一個(gè)線程,或通過硬件更新的字段對訪問進(jìn)行同步。

從上面的文字看不出他和代碼優(yōu)化有任何關(guān)聯(lián),那接著往下看。

volatile關(guān)鍵字則是volatile構(gòu)造的另外一種實(shí)現(xiàn)方式,它是VolatileRead和VolatileWrite的簡化版,使用 volatile 修飾符對字段可以保證對該字段的所有訪問都使用 VolatileRead 或 VolatileWrite。MSDN中對volatile關(guān)鍵字的說明是

volatile 關(guān)鍵字指示一個(gè)字段可以由多個(gè)同時(shí)執(zhí)行的線程修改。 聲明為 volatile 的字段不受編譯器優(yōu)化(假定由單個(gè)線程訪問)的限制。 這樣可以確保該字段在任何時(shí)間呈現(xiàn)的都是最新的值。

從這里可以看出跟代碼優(yōu)化有關(guān)系了。而縱觀上面的介紹得出兩個(gè)結(jié)論:

1.使用了volatile構(gòu)造的字段讀寫都是直接對內(nèi)存操作,不涉及CPU寄存器,使得所有線程對它的讀寫都是同步,不存在臟讀了。讀操作是原子的,寫操作也是原子的。

2.使用了volatile構(gòu)造修飾(或訪問)字段,它會嚴(yán)格按照代碼編寫的順序執(zhí)行,讀操作將會在最早執(zhí)行,寫操作將會最遲執(zhí)行。

最后一個(gè)volatile構(gòu)造是在.NET Framework中新增的,里面包含的方法都是Read和Write,它實(shí)際上就相當(dāng)于Thread的VolatileRead 和VolatileWrite 。這需要拿源碼來說明了,隨便拿一個(gè)Volatile的Read方法來看

多線程之線程同步

而再看看Thraed的VolatileRead方法

多線程之線程同步

   

另一個(gè)用戶模式構(gòu)造是Interlocked,這個(gè)構(gòu)造是保證讀和寫都是在原子操作里面,這是與上面volatile最大的區(qū)別,volatile只能確保單純的讀或者單純的寫。

為何Interlocked是這樣,看一下Interlocaked的方法就知道了

Add(ref int,int)// 調(diào)用ExternAdd 外部方法

CompareExchange(ref Int32,Int32,Int32)//1與3是否相等,相等則替換2,返回1的原始值

Decrement(ref Int32)//遞減并返回 調(diào)用add

Exchange(ref Int32,Int32)//將2設(shè)置到1并返回

Increment(ref Int32)//自增 調(diào)用add

就隨便拿其中一個(gè)方法Add(ref int,int)來說(Increment和Decrement這兩個(gè)方法實(shí)際上內(nèi)部調(diào)用了Add方法),它會先讀到第一個(gè)參數(shù)的值,在與第二個(gè)參數(shù)求和后,把結(jié)果寫到給第一參數(shù)中。首先這整個(gè)過程是一個(gè)原子操作,在這個(gè)操作里面既包含了讀,也包含了寫。至于如何保證這個(gè)操作的原子性,估計(jì)需要查看Rotor源碼才行。在代碼優(yōu)化方面來說,它確保了所有寫操作都在Interlocked之前去執(zhí)行,這保證了Interlocked里面用到的值是最新的;而任何變量的讀取都在Interlocked之后讀取,這保證了后面用到的值都是最新更改過的。

CompareExchange方法相當(dāng)重要,雖然Interlocked提供的方法甚少,但基于這個(gè)可以擴(kuò)展出其他更多方法,下面就是個(gè)例子,求出兩個(gè)值的最大值,直接抄了Jeffrey的源碼

多線程之線程同步

查看上面代碼,在進(jìn)入循環(huán)之前先聲明每次循環(huán)開始時(shí)target的值,在求出最值之后,核對一下target的值是否有變化,如果有變化則需要再記錄新值,按照新值來再求一次最值,直到target不變?yōu)橹?,這就滿足了Interlocked中所說的,寫都在Interlocked之前發(fā)生,Interlocked往后就能讀到最新的值。

   

基元內(nèi)核模式

內(nèi)核模式則是靠操作系統(tǒng)的內(nèi)核對象來處理線程的同步問題。先說其弊端,它的速度會相對慢。原因有兩個(gè),其一由于它是由操作系統(tǒng)內(nèi)核對象來實(shí)現(xiàn)的,需要操作系統(tǒng)內(nèi)部去協(xié)調(diào),另外一個(gè)原因是內(nèi)核對象都是一些非托管對象,在了解了AppDomain之后就會知道,訪問的對象不在當(dāng)前AppDomain中的要么就進(jìn)行按值封送,要么就進(jìn)行按引用封送。經(jīng)過觀察這部分的非托管資源是按引用封送,這就會存在性能影響。綜合上面兩方面的兩點(diǎn)得出內(nèi)核模式的弊端。但是他也是有利的方面:1.線程在等待資源的時(shí)候不會"自旋"而是阻塞,這個(gè)節(jié)省了CPU時(shí)間,并且這個(gè)阻塞可以設(shè)定一個(gè)超時(shí)值。2.可以實(shí)現(xiàn)Window線程和CLR線程的同步,也可同步不同進(jìn)程中的線程(前者未體驗(yàn)到,而對于后者則知道semaphores中有邊界值資源)。3.可應(yīng)用安全性設(shè)置,為經(jīng)授權(quán)賬戶禁止訪問(這個(gè)不知道是咋回事)。

內(nèi)核模式的所有對象的基類是WaitHandle。內(nèi)核模式的所有類層次如下

WaitHandle

EventWaitHandle

AutoResetEvent

ManualResetEvent

Semaphore

Mutex

   

WaitHandle繼承MarshalByRefObject,這個(gè)就是按引用封送了非托管對象。WaitHandle里面主要是各種Wait方法,調(diào)用了Wait方法在沒有收到信號之前會被阻塞。WaitOne則是等待一個(gè)信號,WaitAny(WaitHandle[] waitHandles)則是收到任意一個(gè)waitHandles的信號,WaitAll(WaitHandle[] waitHandles)則是等待所有waitHandles的信號。這些方法都有一個(gè)版本允許設(shè)置一個(gè)超時(shí)時(shí)間。其他的內(nèi)核模式構(gòu)造都有類似的Wait方法。

EventWaitHandle的內(nèi)部維護(hù)著一個(gè)布爾值,而Wait方法會在這個(gè)布爾值為false時(shí)線程就會被阻塞,直到該布爾值為true時(shí)線程才被釋放。操縱這個(gè)布爾值的方法有Set()和Reset(),前者是把布爾值設(shè)成true;后者則設(shè)成false。這相當(dāng)于一個(gè)開關(guān),調(diào)用了Reset之后線程執(zhí)行到Wait就暫停了,直到Set才恢復(fù)。它有兩個(gè)子類,使用的方式類似,區(qū)別在于AutoResetEvent調(diào)用Set之后自動(dòng)調(diào)用Reset,使得開關(guān)馬上恢復(fù)關(guān)閉狀態(tài);而ManualResetEvent就需要手動(dòng)調(diào)用Set讓開關(guān)關(guān)閉。這樣就達(dá)到一個(gè)效果一般情況下AutoResetEvent每次釋放的時(shí)候能讓一條線程通過;而ManualResetEvent在手動(dòng)調(diào)用Reset之前有可能會讓多條線程通過。

Semaphore的內(nèi)部是維護(hù)著一個(gè)×××,當(dāng)構(gòu)造一個(gè)Semaphore對象時(shí)會指定最大的信號量與初始信號量值,每當(dāng)調(diào)用一次WaitOne,信號量就會加1,當(dāng)加到最大值時(shí),線程就會被阻塞,當(dāng)調(diào)用Release的時(shí)候就會釋放一個(gè)或多個(gè)信號量,此時(shí)被阻塞掉的一個(gè)或多個(gè)線程就會被釋放。這個(gè)就符合生產(chǎn)者與消費(fèi)者問題了,當(dāng)生產(chǎn)者不斷往產(chǎn)品隊(duì)列中加入產(chǎn)品時(shí),他就會WaitOne,當(dāng)隊(duì)列滿了,就相當(dāng)于信號量滿了,生成者就會被阻塞,當(dāng)消費(fèi)者消費(fèi)掉一個(gè)商品時(shí),就會Release釋放掉產(chǎn)品隊(duì)列中的一個(gè)空間,此時(shí)因沒有空間存放產(chǎn)品的生產(chǎn)者又可以開始工作往產(chǎn)品隊(duì)列中存放產(chǎn)品了。

Mutex的內(nèi)部與規(guī)則相對前面兩者稍微復(fù)雜一點(diǎn),先說與前面相似的地方就是同樣都會通過WaitOne來阻塞當(dāng)前線程,通過ReleastMutex來釋放對線程的阻塞。區(qū)別在于WaitOne的允許第一個(gè)調(diào)用的線程通過,其余后面的線程調(diào)用到WaitOne就會被阻塞,通過了WaitOne的線程可以重復(fù)調(diào)用WaitOne多次,但是必須調(diào)用同樣次數(shù)的ReleaseMutex來釋放,否則會因?yàn)榇螖?shù)不對等導(dǎo)致別的線程一直處于阻塞的狀態(tài)。相比起之前的幾個(gè)構(gòu)造,這個(gè)構(gòu)造會有線程所有權(quán)與遞歸這兩個(gè)概念,這個(gè)是單純靠前面的構(gòu)造都無法實(shí)現(xiàn)的,額外封裝除外。

   

混合構(gòu)造

上面的基元構(gòu)造是用了最簡單的實(shí)現(xiàn)方式,用戶 模式有用戶模式的快,但是它會帶來CPU時(shí)間的浪費(fèi);內(nèi)核模式解決了這個(gè)問題,但是會帶來性能上的損失,各有利弊,而混合構(gòu)造則是集合了兩者的利,它會在內(nèi)部通過一定策略適當(dāng)?shù)臅r(shí)機(jī)使用用戶模式,再另一種情況下又會使用內(nèi)核模式。但是這些層層判斷帶來的是內(nèi)存上的開銷。在多線程同步中沒有完美的構(gòu)造,各個(gè)構(gòu)造都有利弊,存在即有意義,結(jié)合具體的應(yīng)用場景就會有最優(yōu)的構(gòu)造可供使用。只是在于我們能否按照具體的場景權(quán)衡利弊而已。

各種Slim后綴的類,在System.Threading命名空間中,可以看到若干個(gè)以Slim后綴結(jié)尾的類:ManualResetEventSlim,SemaphoreSlim,ReaderWriterLockSlim。除了最后一個(gè),其余兩個(gè)都是在基元內(nèi)核模式中有一樣的構(gòu)造,但是這三個(gè)類都是原有構(gòu)造的簡化版,尤其是前兩個(gè),使用方式跟原有的一樣,但是盡量避免使用操作系統(tǒng)的內(nèi)核對象,而達(dá)到了輕量級的效果。比如在SemaphoreSlim中使用了內(nèi)核構(gòu)造ManualResetEvent,但是這個(gè)構(gòu)造是通過延時(shí)初始化,沒達(dá)到非不得已時(shí)都不使用。至于ReaderWriterLockSlim則在后面再介紹。

Monitor與lock,lock關(guān)鍵字可謂是最廣為人知的一種實(shí)現(xiàn)多線程同步的手段,那么下面則又從一段代碼說起

多線程之線程同步

這個(gè)方法相當(dāng)簡單且無實(shí)際意義,它只是為了看編譯器把這段代碼編譯成什么樣子,通過查看IL如下

多線程之線程同步

留意到IL代碼中出現(xiàn)了try…finally語句塊、Monitor.Enter與Monotor.Exit方法。然后把代碼更改一下再編譯看看IL

多線程之線程同步

IL代碼

多線程之線程同步

代碼比較相似,但并非等價(jià),實(shí)際上與lock語句塊等價(jià)的代碼如下

多線程之線程同步

那么既然lock本質(zhì)上是調(diào)用了Monitor,那Monitor是如何通過對一個(gè)對象加鎖,然后實(shí)現(xiàn)線程同步。原來每個(gè)在托管堆里面的對象都有兩個(gè)固定的成員,一個(gè)指向該對象類型的指針,另一個(gè)是指向一個(gè)線程同步塊索引。這個(gè)索引指向一個(gè)同步塊數(shù)組的元素,Monitor對線程加鎖就是靠這個(gè)同步塊。按照J(rèn)effrey(CLR via C#的作者)的說法同步塊中有三個(gè)字段,所有權(quán)的線程Id,等待線程的數(shù)量,遞歸的次數(shù)。然而我通過另一批文章了解到線程同步塊的成員并非單純這幾個(gè),有興趣的同學(xué)可以去閱讀《揭示同步塊索引》的文章,有兩篇。 當(dāng)Monitor需要為某個(gè)對象obj加鎖時(shí),它會檢查obj的同步塊索引有否為數(shù)組的某個(gè)索引,如果是-1的,則從數(shù)組中找出一個(gè)空閑的同步塊與之關(guān)聯(lián),同時(shí)同步塊的所有權(quán)線程Id就記錄下當(dāng)前線程的Id;當(dāng)再次有線程調(diào)用Monitor的時(shí)候就會檢查同步塊的所有權(quán)Id和當(dāng)前線程Id是否對應(yīng)上,能對應(yīng)上的就讓其通過,在遞歸次數(shù)上加1,如果對應(yīng)不上的就把該線程扔到一個(gè)就緒隊(duì)列(這個(gè)隊(duì)列實(shí)際上也是存在同步塊里面)中,并將其阻塞;這個(gè)同步塊會在調(diào)用Exit的時(shí)候檢查遞歸次數(shù)確保遞歸完了就清除所有權(quán)線程Id。通過等待線程數(shù)量得知是否有線程在等待,如果有則從等待隊(duì)列中取出線程并釋放,否則就解除與同步塊的關(guān)聯(lián),讓同步塊等待被下個(gè)被加鎖的對象使用。

Monitor中還有一對方法Wait與Pulse。前者可以使得獲得到鎖的線程短暫地將鎖釋放,而當(dāng)前線程就會被阻塞而放入等待隊(duì)列中。直到其他線程調(diào)用了Pulse方法,才會從等待隊(duì)列中把線程放到就緒隊(duì)列中,等待下次鎖被釋放時(shí),才有機(jī)會被再次獲取鎖,具體能否獲取就要看等待隊(duì)列中的情況了。

ReaderWriterLock讀寫鎖,傳統(tǒng)的lock關(guān)鍵字(即等價(jià)于Monitor的Enter和Exit),他對共享資源的鎖是全互斥鎖,一經(jīng)加鎖的資源其他資源完全不能訪問。

ReaderWriterLock對互斥資源的加的鎖分讀鎖與寫鎖,類似于數(shù)據(jù)庫中提到的共享鎖和排他鎖。大致情況是加了讀鎖的資源允許多個(gè)線程對其訪問,而加了寫鎖的資源只有一個(gè)線程可以對其訪問。兩種加了不同縮的線程都不能同時(shí)訪問資源,而嚴(yán)格來說,加了讀鎖的線程只要在同一個(gè)隊(duì)列中的都能訪問資源,而不同隊(duì)列的則不能訪問;加了寫鎖的資源只能在一個(gè)隊(duì)列中,而寫鎖隊(duì)列中只有一個(gè)線程能訪問資源。區(qū)分讀鎖的線程是否在于統(tǒng)一個(gè)隊(duì)列中的判斷標(biāo)準(zhǔn)是,本次加讀鎖的線程與上次加讀鎖的線程這個(gè)時(shí)間段中,有否別的線程加了寫鎖,沒沒別的線程加寫鎖,則這兩個(gè)線程都在同一個(gè)讀鎖隊(duì)列中。

ReaderWriterLockSlimReaderWriterLock類似,是后者的升級版,出現(xiàn)在.NET Framework3.5,據(jù)說是優(yōu)化了遞歸和簡化了操作。在此遞歸策略我尚未深究過。目前大概列舉一下它們通常用的方法

ReaderWriterLock常用的方法

Acqurie或Release ReaderLock或WriteLock 的排列組合

UpGradeToWriteLock/DownGradeFromWriteLock 用于在讀鎖中升級到寫鎖。當(dāng)然在這個(gè)升級的過程中也涉及到線程從讀鎖隊(duì)列切換到寫鎖隊(duì)列中,因此需要等待。

ReleaseLock/RestoreLock 釋放所有鎖和恢復(fù)鎖狀態(tài)

   

ReaderWriterLock實(shí)現(xiàn)IDispose接口,其方法則是以下模式

TryEnter/Enter/Exit ReadLock/WriteLock/UpGradeableReadLock

(以上內(nèi)容引用自另一篇筆記《ReaderWriterLock)

CoutdownEvent比較少用的混合構(gòu)造,這個(gè)跟Semaphore相反,體現(xiàn)在Semaphore是在內(nèi)部計(jì)數(shù)(也就是信號量)達(dá)到最大值的時(shí)候讓線程阻塞,而CountdownEvent是在內(nèi)部計(jì)數(shù)達(dá)到0的時(shí)候才讓線程阻塞。其方法有

AddCount //計(jì)數(shù)遞增;

Signal //計(jì)數(shù)遞減;

Reset //計(jì)數(shù)重設(shè)為指定或初始;

Wait //當(dāng)且僅當(dāng)計(jì)數(shù)為0才不阻塞,否則就阻塞。

Barrier也是一個(gè)比較少用的混合構(gòu)造,用于處理多線程在分步驟的操作中協(xié)作問題。它內(nèi)部維護(hù)著一個(gè)計(jì)數(shù),該計(jì)數(shù)代表這次協(xié)作的參與者數(shù)量,當(dāng)不同的線程調(diào)用SignalAndWait的時(shí)候會給這個(gè)計(jì)數(shù)加1并且把調(diào)用的線程阻塞,直到計(jì)數(shù)達(dá)到最大值的時(shí)候,才會釋放所有被阻塞的線程。假設(shè)還是不明白的話就看一下MSND上面的示例代碼

多線程之線程同步

這里給Barrier初始化的參與者數(shù)量是3,同時(shí)每完成一個(gè)步驟的時(shí)候會調(diào)用委托,該方法是輸出count的值步驟索引。參與者數(shù)量后來增加了兩個(gè)又減少了一個(gè)。每個(gè)參與者的操作都是相同,給count進(jìn)行原子自增,自增完則調(diào)用SgnalAndWait告知Barrier當(dāng)前步驟已完成并等待下一個(gè)步驟的開始。但是第三次由于回調(diào)方法里拋出了一個(gè)異常,每個(gè)參與者在調(diào)用SignalAndWait的時(shí)候都會拋出一個(gè)異常。通過Parallel開始了一個(gè)并行操作。假設(shè)并行開的作業(yè)數(shù)跟Barrier參與者數(shù)量不一樣就會導(dǎo)致在SignalAndWait會有非預(yù)期的情況出現(xiàn)。

接下來說兩個(gè)Attribute,這個(gè)估計(jì)不算是同步構(gòu)造,但是也能在線程同步中發(fā)揮作用

MethodImplAttribute這個(gè)Attribute適用于方法的,當(dāng)給定的參數(shù)是MethodImplOptions.Synchronized,它會對整個(gè)方法的方法體進(jìn)行加鎖,凡是調(diào)用這個(gè)方法的線程在沒有獲得鎖的時(shí)候就會被阻塞,直到擁有鎖的線程釋放了才將其喚醒。對靜態(tài)方法而言它就相當(dāng)于把該類的類型對象給鎖了,即lock(typeof(ClassType));對于實(shí)例方法他就相當(dāng)于把該對象的實(shí)例給鎖了,即lock(this)。最開始對它內(nèi)部調(diào)用了lock這個(gè)結(jié)論存在猜疑,于是用IL編譯了一下,發(fā)現(xiàn)方法體的代碼沒啥異樣,查看了一些源碼也好無頭緒,后來發(fā)現(xiàn)它的IL方法頭跟普通的方法有區(qū)別,多了一個(gè)synchronized

多線程之線程同步

于是網(wǎng)上找各種資料,最后發(fā)現(xiàn)"junchu25"的博客[1][2]里提到用WinDbg來查看JIT生成的代碼。

調(diào)用Attribute的

多線程之線程同步

調(diào)用lock的

多線程之線程同步

對于用這個(gè)Attribute實(shí)現(xiàn)的線程同步連Jeffrey都不推薦使用。

System.Runtime.Remoting.Contexts.SynchronizationAttribute這個(gè)Attribute適用于類,在類的定義中加了這個(gè)Attribute并繼承與ContextBoundOject的類,它會對類中的所有方法都加上同一個(gè)鎖,對比MethodImplAttribute它的范圍更廣,當(dāng)一個(gè)線程調(diào)用此類的任何方法時(shí),如果沒有獲得鎖,那么該線程就會被阻塞。有個(gè)說法是它本質(zhì)上調(diào)用了lock,對于這個(gè)說法的求證就更不容易,國內(nèi)的資源少之又少,里面又涉及到AppDomain,線程上下文,最后核心的就是由SynchronizedServerContextSink這個(gè)類去實(shí)現(xiàn)的。AppDomain應(yīng)該要另立篇進(jìn)行介紹。但是在這里也要稍微說一下,以前以為內(nèi)存中就是有線程棧與堆內(nèi)存,而這只是很基本的劃分,堆內(nèi)存還會劃分成若干個(gè)AppDomain,在每個(gè)AppDomain中也至少有一個(gè)上下文,每個(gè)對象都會從屬與一個(gè)AppDomain里面的一個(gè)上下文中???/span>AppDomain的對象是不能直接訪問的,要么進(jìn)行按值封送(相當(dāng)于深復(fù)制一個(gè)對象到調(diào)用的AppDomain),要么就按引用封送。對于按引用封送則需要該類繼承MarshalByRefObject。對繼承了這個(gè)類的對象進(jìn)行調(diào)用時(shí)都不是調(diào)用類的本身,而是通過代理的形式進(jìn)行調(diào)用。那么跨上下文的也需要進(jìn)行按值封送操作。平常構(gòu)造的一個(gè)對象都是在進(jìn)程默認(rèn)AppDomain下的默認(rèn)上下文中,而使用了SynchronizationAttribute特性的類它的實(shí)例是屬于另外的一個(gè)上下文中,繼承了ContextBoundObject基類的類進(jìn)行跨上下文訪問對象時(shí)也是通過按引用封送的方式用代理訪問對象,并非訪問到對象本身。至于是否跨上下文訪問對象可以通過的RemotingServices.IsObjectOutOfContext(obj)方法進(jìn)行判斷。SynchronizedServerContextSink是mscorlib的一個(gè)內(nèi)部類。當(dāng)線程調(diào)用跨上下文的對象時(shí),這個(gè)調(diào)用會被SynchronizedServerContextSink封裝成WorkItem的對象,該對象也mscorlib的中的一個(gè)內(nèi)部類,SynchronizedServerContextSink就請求SynchronizationAttribute,Attribute根據(jù)現(xiàn)在是否有多個(gè)WorkItem的執(zhí)行請求來決定當(dāng)前處理的這個(gè)WorkItem會馬上執(zhí)行還是放到一個(gè)先進(jìn)先出的WorkItem隊(duì)列中按順序執(zhí)行,這個(gè)隊(duì)列是SynchronizationAttribute的一個(gè)成員,隊(duì)列成員入隊(duì)出隊(duì)時(shí)或者Attribute判斷是否馬上執(zhí)行WorkItem時(shí)都需要獲取一個(gè)lock的鎖,被鎖的對象也正是這個(gè)WorkItem的隊(duì)列。這里面涉及到幾個(gè)類的交互,鄙人現(xiàn)在還沒完全看清,以上這個(gè)處理過程可能有錯(cuò),待分析清楚再進(jìn)行補(bǔ)充。不過通過這個(gè)Attribute實(shí)現(xiàn)的線程同步按鄙人的直覺也是不推薦使用的,主要是性能方面的損耗,鎖的范圍也比較大。


向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI