• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            S.l.e!ep.¢%

            像打了激速一樣,以四倍的速度運(yùn)轉(zhuǎn),開(kāi)心的工作
            簡(jiǎn)單、開(kāi)放、平等的公司文化;尊重個(gè)性、自由與個(gè)人價(jià)值;
            posts - 1098, comments - 335, trackbacks - 0, articles - 1
              C++博客 :: 首頁(yè) :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理
            并發(fā)危險(xiǎn)
            解決多線程代碼中的 11 個(gè)常見(jiàn)的問(wèn)題
            Joe Duffy

            本文將介紹以下內(nèi)容:
            • 基本并發(fā)概念
            • 并發(fā)問(wèn)題和抑制措施
            • 實(shí)現(xiàn)安全性的模式
            • 橫切概念
            本文使用了以下技術(shù):
            多線程、.NET Framework
            并發(fā)現(xiàn)象無(wú)處不在。 服務(wù)器端程序長(zhǎng)久以來(lái)都必須負(fù)責(zé)處理基本并發(fā)編程模型,而隨著多核處理器的日益普及,客戶端程序也將需要執(zhí)行一些任務(wù)。隨著并發(fā)操作的不斷增加,有關(guān)確保安全的問(wèn)題也浮現(xiàn)出來(lái)。也就是說(shuō),在面對(duì)大量邏輯并發(fā)操作和不斷變化的物理硬件并行性程度時(shí),程序必須繼續(xù)保持同樣級(jí)別的穩(wěn)定性和可靠性。
            與對(duì)應(yīng)的順序代碼相比,正確設(shè)計(jì)的并發(fā)代碼還必須遵循一些額外的規(guī)則。對(duì)內(nèi)存的讀寫(xiě)以及對(duì)共享資源的訪問(wèn)必須使用同步機(jī)制進(jìn)行管制,以防發(fā)生沖突。另外,通常有必要對(duì)線程進(jìn)行協(xié)調(diào)以協(xié)同完成某項(xiàng)工作。
            這些附加要求所產(chǎn)生的直接結(jié)果是,可以從根本上確保線程始終保持一致并且保證其順利向前推進(jìn)。同步和協(xié)調(diào)對(duì)時(shí)間的依賴性很強(qiáng),這就導(dǎo)致了它們具有不確定性,難于進(jìn)行預(yù)測(cè)和測(cè)試。
            這些屬性之所以讓人覺(jué)得有些困難,只是因?yàn)槿藗兊乃悸愤€未轉(zhuǎn)變過(guò)來(lái)。沒(méi)有可供學(xué)習(xí)的專門(mén) API,也沒(méi)有可進(jìn)行復(fù)制和粘貼的代碼段。實(shí)際上的確有一組基礎(chǔ)概念需要您學(xué)習(xí)和適應(yīng)。很可能隨著時(shí)間的推移某些語(yǔ)言和庫(kù)會(huì)隱藏一些概念,但如果您現(xiàn)在就開(kāi)始執(zhí)行并發(fā)操作,則不會(huì)遇到這種情況。本文將介紹需要注意的一些較為常見(jiàn)的挑戰(zhàn),并針對(duì)您在軟件中如何運(yùn)用它們給出一些建議。
            首先我將討論在并發(fā)程序中經(jīng)常會(huì)出錯(cuò)的一類問(wèn)題。我把它們稱為“安全隱患”,因?yàn)樗鼈兒苋菀装l(fā)現(xiàn)并且后果通常比較嚴(yán)重。這些危險(xiǎn)會(huì)導(dǎo)致您的程序因崩潰或內(nèi)存問(wèn)題而中斷。

            當(dāng)從多個(gè)線程并發(fā)訪問(wèn)數(shù)據(jù)時(shí)會(huì)發(fā)生數(shù)據(jù)爭(zhēng)用(或競(jìng)爭(zhēng)條件)。特別是,在一個(gè)或多個(gè)線程寫(xiě)入一段數(shù)據(jù)的同時(shí),如果有一個(gè)或多個(gè)線程也在讀取這段數(shù)據(jù),則會(huì)發(fā)生這種情況。之所以會(huì)出現(xiàn)這種問(wèn)題,是因?yàn)?Windows 程序(如 C++ 和 Microsoft .NET Framework 之類的程序)基本上都基于共享內(nèi)存概念,進(jìn)程中的所有線程均可訪問(wèn)駐留在同一虛擬地址空間中的數(shù)據(jù)。靜態(tài)變量和堆分配可用于共享。
            請(qǐng)考慮下面這個(gè)典型的例子:
            static class Counter {
                internal static int s_curr = 0;
                internal static int GetNext() { 
                    return s_curr++; 
                }
            }
            
            Counter 的目標(biāo)可能是想為 GetNext 的每個(gè)調(diào)用分發(fā)一個(gè)新的唯一數(shù)字。但是,如果程序中的兩個(gè)線程同時(shí)調(diào)用 GetNext,則這兩個(gè)線程可能被賦予相同的數(shù)字。原因是 s_curr++ 編譯包括三個(gè)獨(dú)立的步驟:
            1. 將當(dāng)前值從共享的 s_curr 變量讀入處理器寄存器。
            2. 遞增該寄存器。
            3. 將寄存器值重新寫(xiě)入共享 s_curr 變量。
            按照這種順序執(zhí)行的兩個(gè)線程可能會(huì)在本地從 s_curr 讀取了相同的值(比如 42)并將其遞增到某個(gè)值(比如 43),然后發(fā)布相同的結(jié)果值。這樣一來(lái),GetNext 將為這兩個(gè)線程返回相同的數(shù)字,導(dǎo)致算法中斷。雖然簡(jiǎn)單語(yǔ)句 s_curr++ 看似不可分割,但實(shí)際卻并非如此。

            忘記同步
            這是最簡(jiǎn)單的一種數(shù)據(jù)爭(zhēng)用情況:同步被完全遺忘。這種爭(zhēng)用很少有良性的情況,也就是說(shuō)雖然它們是正確的,但大部分都是因?yàn)檫@種正確性的根基存在問(wèn)題。
            這種問(wèn)題通常不是很明顯。例如,某個(gè)對(duì)象可能是某個(gè)大型復(fù)雜對(duì)象圖表的一部分,而該圖表恰好可使用靜態(tài)變量訪問(wèn),或在創(chuàng)建新線程或?qū)⒐ぷ髋湃刖€程池時(shí)通過(guò)將某個(gè)對(duì)象作為閉包的一部分進(jìn)行傳遞可變?yōu)楣蚕韴D表。
            當(dāng)對(duì)象(圖表)從私有變?yōu)楣蚕頃r(shí),一定要多加注意。這稱為發(fā)布,在后面的隔離上下文中會(huì)對(duì)此加以討論。反之稱為私有化,即對(duì)象(圖表)再次從共享變?yōu)樗接小?/div>
            對(duì)這種問(wèn)題的解決方案是添加正確的同步。在計(jì)數(shù)器示例中,我可以使用簡(jiǎn)單的聯(lián)鎖:
            static class Counter {
                internal static volatile int s_curr = 0;
                internal static int GetNext() { 
                    return Interlocked.Increment(ref s_curr); 
                }
            }
            
            它之所以起作用,是因?yàn)楦卤幌薅ㄔ趩我粌?nèi)存位置,還因?yàn)椋ㄟ@一點(diǎn)非常方便)存在硬件指令 (LOCK INC),它相當(dāng)于我嘗試進(jìn)行原子化操作的軟件語(yǔ)句。
            或者,我可以使用成熟的鎖定:
            static class Counter {
                internal static int s_curr = 0;
                private static object s_currLock = new object();
                internal static int GetNext() {
                    lock (s_currLock) { 
                        return s_curr++; 
                    }
                }
            }
            
            lock 語(yǔ)句可確保試圖訪問(wèn) GetNext 的所有線程彼此之間互斥,并且它使用 CLR System.Threading.Monitor 類。C++ 程序使用 CRITICAL_SECTION 來(lái)實(shí)現(xiàn)相同目的。雖然對(duì)這個(gè)特定的示例不必使用鎖定,但當(dāng)涉及多個(gè)操作時(shí),幾乎不可能將其并入單個(gè)互鎖操作中。

            粒度錯(cuò)誤
            即使使用正確的同步對(duì)共享狀態(tài)進(jìn)行訪問(wèn),所產(chǎn)生的行為仍然可能是錯(cuò)誤的。粒度必須足夠大,才能將必須視為原子的操作封裝在此區(qū)域中。這將導(dǎo)致在正確性與縮小區(qū)域之間產(chǎn)生沖突,因?yàn)榭s小區(qū)域會(huì)減少其他線程等待同步進(jìn)入的時(shí)間。
            例如,讓我們看一看圖 1 所示的銀行帳戶抽象。一切都很正常,對(duì)象的兩個(gè)方法(Deposit 和 Withdraw)看起來(lái)不會(huì)發(fā)生并發(fā)錯(cuò)誤。一些銀行業(yè)應(yīng)用程序可能會(huì)使用它們,而且不擔(dān)心余額會(huì)因?yàn)椴l(fā)訪問(wèn)而遭到損壞。
            class BankAccount {
                private decimal m_balance = 0.0M;
                private object m_balanceLock = new object();
                internal void Deposit(decimal delta) {
                    lock (m_balanceLock) { m_balance += delta; }
                }
                internal void Withdraw(decimal delta) {
                    lock (m_balanceLock) {
                        if (m_balance < delta)
                            throw new Exception("Insufficient funds");
                        m_balance -= delta;
                    }
                }
            }
            
            但是,如果您想添加一個(gè) Transfer 方法該怎么辦?一種天真的(也是不正確的)想法會(huì)認(rèn)為由于 Deposit 和 Withdraw 是安全隔離的,因此很容易就可以合并它們:
            class BankAccount {
                internal static void Transfer(
                  BankAccount a, BankAccount b, decimal delta) {
                    Withdraw(a, delta);
                    Deposit(b, delta);
                }
                // As before 
            }
            
            這是不正確的。實(shí)際上,在執(zhí)行 Withdraw 與 Deposit 調(diào)用之間的一段時(shí)間內(nèi)資金會(huì)完全丟失。
            正確的做法是必須提前對(duì) a 和 b 進(jìn)行鎖定,然后再執(zhí)行方法調(diào)用:
            class BankAccount {
                internal static void Transfer(
                  BankAccount a, BankAccount b, decimal delta) {
                    lock (a.m_balanceLock) {
                        lock (b.m_balanceLock) {
                            Withdraw(a, delta);
                            Deposit(b, delta);
                        }
                    }
                }
                // As before 
            }
            
            事實(shí)證明,此方法可解決粒度問(wèn)題,但卻容易發(fā)生死鎖。稍后,您會(huì)了解到如何修復(fù)它。

            讀寫(xiě)撕裂
            如前所述,良性爭(zhēng)用允許您在沒(méi)有同步的情況下訪問(wèn)變量。對(duì)于那些對(duì)齊的、自然分割大小的字 — 例如,用指針?lè)指畲笮〉膬?nèi)容在 32 位處理器中是 32 位的(4 字節(jié)),而在 64 位處理器中則是 64 位的(8 字節(jié))— 讀寫(xiě)操作是原子的。如果某個(gè)線程只讀取其他線程將要寫(xiě)入的單個(gè)變量,而沒(méi)有涉及任何復(fù)雜的不變體,則在某些情況下您完全可以根據(jù)這一保證來(lái)略過(guò)同步。
            但要注意。如果試圖在未對(duì)齊的內(nèi)存位置或未采用自然分割大小的位置這樣做,可能會(huì)遇到讀寫(xiě)撕裂現(xiàn)象。之所以發(fā)生撕裂現(xiàn)象,是因?yàn)榇祟愇恢玫淖x或?qū)憣?shí)際上涉及多個(gè)物理內(nèi)存操作。它們之間可能會(huì)發(fā)生并行更新,并進(jìn)而導(dǎo)致其結(jié)果可能是之前的值和之后的值通過(guò)某種形式的組合。
            例如,假設(shè) ThreadA 處于循環(huán)中,現(xiàn)在需要僅將 0x0L 和 0xaaaabbbbccccddddL 寫(xiě)入 64 位變量 s_x 中。ThreadB 在循環(huán)中讀取它(參見(jiàn)圖 2)。
            internal static volatile long s_x;
            void ThreadA() {
                int i = 0;
                while (true) {
                    s_x = (i & 1) == 0 ? 0x0L : 0xaaaabbbbccccddddL;
                    i++;
                }
            }
            void ThreadB() {
                while (true) {
                    long x = s_x;
                    Debug.Assert(x == 0x0L || x == 0xaaaabbbbccccddddL);
                }
            }
            
            您可能會(huì)驚訝地發(fā)現(xiàn) ThreadB 的聲明可能會(huì)被觸發(fā)。原因是 ThreadA 的寫(xiě)入操作包含兩部分(高 32 位和低 32 位),具體順序取決于編譯器。ThreadB 的讀取也是如此。因此 ThreadB 可以見(jiàn)證值 0xaaaabbbb00000000L 或 0x00000000aaaabbbbL。

            無(wú)鎖定重新排序
            有時(shí)編寫(xiě)無(wú)鎖定代碼來(lái)實(shí)現(xiàn)更好的可伸縮性和可靠性是一種非常誘人的想法。這樣做需要深入了解目標(biāo)平臺(tái)的內(nèi)存模型(有關(guān)詳細(xì)信息,請(qǐng)參閱 Vance Morrison 的文章 "Memory Models:Understand the Impact of Low-Lock Techniques in Multithreaded Apps",網(wǎng)址為 msdn.microsoft.com/magazine/cc163715)。如果不了解或不注意這些規(guī)則可能會(huì)導(dǎo)致內(nèi)存重新排序錯(cuò)誤。之所以發(fā)生這些錯(cuò)誤,是因?yàn)榫幾g器和處理器在處理或優(yōu)化期間可自由重新排序內(nèi)存操作。
            例如,假設(shè) s_x 和 s_y 均被初始化為值 0,如下所示:
            internal static volatile int s_x = 0;
            internal static volatile int s_xa = 0;
            internal static volatile int s_y = 0;
            internal static volatile int s_ya = 0;
            
            void ThreadA() {
                s_x = 1;
                s_ya = s_y;
            }
            
            void ThreadB() {
                s_y = 1;
                s_xa = s_x;
            }
            
            是否有可能在 ThreadA 和 ThreadB 均運(yùn)行完成后,s_ya 和 s_xa 都包含值 0?看上去這個(gè)問(wèn)題很可笑。或者 s_x = 1 或者 s_y = 1 會(huì)首先發(fā)生,在這種情況下,其他線程會(huì)在開(kāi)始處理其自身的更新時(shí)見(jiàn)證這一更新。至少理論上如此。
            遺憾的是,處理器隨時(shí)都可能重新排序此代碼,以使在寫(xiě)入之前加載操作更有效。您可以借助一個(gè)顯式內(nèi)存屏障來(lái)避免此問(wèn)題:
            void ThreadA() {
                s_x = 1;
                Thread.MemoryBarrier();
                s_ya = s_y;
            }
            
            .NET Framework 為此提供了一個(gè)特定 API,C++ 提供了 _MemoryBarrier 和類似的宏。但這個(gè)示例并不是想說(shuō)明您應(yīng)該在各處都插入內(nèi)存屏障。它要說(shuō)明的是在完全弄清內(nèi)存模型之前,應(yīng)避免使用無(wú)鎖定代碼,而且即使在完全弄清之后也應(yīng)謹(jǐn)慎行事。

            在 Windows(包括 Win32 和 .NET Framework)中,大多數(shù)鎖定都支持遞歸獲得。這只是意味著,即使當(dāng)前線程已持有鎖但當(dāng)它試圖再次獲得時(shí),其要求仍會(huì)得到滿足。這使得通過(guò)較小的原子操作構(gòu)成較大的原子操作變得更加容易。實(shí)際上,之前給出的 BankAccount 示例依靠的就是遞歸獲得:Transfer 對(duì) Withdraw 和 Deposit 都進(jìn)行了調(diào)用,其中每個(gè)都重復(fù)獲得了 Transfer 已獲得的鎖定。
            但是,如果最終發(fā)生了遞歸獲得操作而您實(shí)際上并不希望如此,則這可能就是問(wèn)題的根源。這可能是因?yàn)橹匦逻M(jìn)入而導(dǎo)致的,而發(fā)生重新進(jìn)入的原因可能是由于對(duì)動(dòng)態(tài)代碼(如虛擬方法和委托)的顯式調(diào)用或由于隱式重新輸入的代碼(如 STA 消息提取和異步過(guò)程調(diào)用)。因此,最好不要從鎖定區(qū)域?qū)?dòng)態(tài)方法進(jìn)行調(diào)用。
            例如,設(shè)想某個(gè)方法暫時(shí)破壞了不變體,然后又調(diào)用委托:
            class C {
                private int m_x = 0;
                private object m_xLock = new object();
                private Action m_action = ...;
            
                internal void M() {
                    lock (m_xLock) {
                        m_x++;
                        try { m_action(); }
                        finally {
                            Debug.Assert(m_x == 1);
                            m_x--;
                        }
                    }
                }
            }
            
            C 的方法 M 可確保 m_x 不發(fā)生改變。但會(huì)有很短的一段時(shí)間,m_x 會(huì)先遞增 1,然后再重新遞減。對(duì) m_action 的調(diào)用看起來(lái)沒(méi)有任何問(wèn)題。遺憾的是,如果它是從 C 類用戶接受的委托,則表示任何代碼都可以執(zhí)行它所請(qǐng)求的操作。這包括回調(diào)到同一實(shí)例的 M 方法。如果發(fā)生了這種情況,finally 中的聲明可能會(huì)被觸發(fā);同一堆棧中可能存在多個(gè)針對(duì) M 的活動(dòng)的調(diào)用(即使您未直接執(zhí)行此操作),這必然會(huì)導(dǎo)致 m_x 包含的值大于 1。

            當(dāng)多個(gè)線程遇到死鎖時(shí),系統(tǒng)會(huì)直接停止響應(yīng)。多篇《MSDN 雜志》文章都介紹了死鎖的發(fā)生原因以及使死鎖變得能夠接受的一些方法,其中包括我自己的文章 "No More Hangs:Advanced Techniques to Avoid and Detect Deadlocks in .NET Apps"(網(wǎng)址為 msdn.microsoft.com/magazine/cc163618)以及 Stephen Toub 的 2007 年 10 月 .NET 相關(guān)問(wèn)題專欄(網(wǎng)址為 msdn.microsoft.com/magazine/cc163352),因此這里只做簡(jiǎn)單的討論。總而言之,只要出現(xiàn)了循環(huán)等待鏈 — 例如,ThreadA 正在等待 ThreadB 持有的資源,而 ThreadB 反過(guò)來(lái)也在等待 ThreadA 持有的資源(也許是間接等待第三個(gè) ThreadC 或其他資源)— 則所有向前的推進(jìn)工作都可能會(huì)停下來(lái)。
            此問(wèn)題的常見(jiàn)根源是互斥鎖。實(shí)際上,之前所示的 BankAccount 示例遇到的就是這個(gè)問(wèn)題。如果 ThreadA 試圖將 $500 從帳戶 #1234 轉(zhuǎn)移到帳戶 #5678,與此同時(shí) ThreadB 試圖將 $500 從 #5678 轉(zhuǎn)移到 #1234,則代碼可能發(fā)生死鎖。
            使用一致的獲得順序可避免死鎖,如圖 3 所示。此邏輯可概括為“同步鎖獲得”之類的名稱,通過(guò)此操作可依照各個(gè)鎖之間的某種順序動(dòng)態(tài)排序多個(gè)可鎖定的對(duì)象,從而使得在以一致的順序獲得兩個(gè)鎖的同時(shí)必須維持兩個(gè)鎖的位置。另一個(gè)方案稱為“鎖矯正”,可用于拒絕被認(rèn)定以不一致的順序完成的鎖獲得。
            class BankAccount {
                private int m_id; // Unique bank account ID.
                internal static void Transfer(
                  BankAccount a, BankAccount b, decimal delta) {
                    if (a.m_id < b.m_id) {
                        Monitor.Enter(a.m_balanceLock); // A first
                        Monitor.Enter(b.m_balanceLock); // ...and then B
                    } else {
                        Monitor.Enter(b.m_balanceLock); // B first
                        Monitor.Enter(a.m_balanceLock); // ...and then A 
                    }
                    try {
                        Withdraw(a, delta);
                        Deposit(b, delta);
                    } finally {
                        Monitor.Exit(a.m_balanceLock);
                        Monitor.Exit(b.m_balanceLock);
                    }
                }
                // As before ...
            }
            
            但鎖并不是導(dǎo)致死鎖的唯一根源。喚醒丟失是另一種現(xiàn)象,此時(shí)某個(gè)事件被遺漏,導(dǎo)致線程永遠(yuǎn)休眠。在 Win32 自動(dòng)重置和手動(dòng)重置事件、CONDITION_VARIABLE、CLR Monitor.Wait、Pulse 以及 PulseAll 調(diào)用等同步事件中經(jīng)常會(huì)發(fā)生這種情況。喚醒丟失通常是一種跡象,表示同步不正確,無(wú)法重置等待條件或在 wake-all(WakeAllConditionVariable 或 Monitor.PulseAll)更為適用的情況下使用了 wake-single 基元(WakeConditionVariable 或 Monitor.Pulse)。
            此問(wèn)題的另一個(gè)常見(jiàn)根源是自動(dòng)重置事件和手動(dòng)重置事件信號(hào)丟失。由于此類事件只能處于一個(gè)狀態(tài)(有信號(hào)或無(wú)信號(hào)),因此用于設(shè)置此事件的冗余調(diào)用實(shí)際上將被忽略不計(jì)。如果代碼認(rèn)定要設(shè)置的兩個(gè)調(diào)用始終需要轉(zhuǎn)換為兩個(gè)喚醒的線程,則結(jié)果可能就是喚醒丟失。

            鎖保護(hù)
            當(dāng)某個(gè)鎖的到達(dá)率與其鎖獲得率相比始終居高不下時(shí),可能會(huì)產(chǎn)生鎖保護(hù)。在極端的情況下,等待某個(gè)鎖的線程超過(guò)了其承受力,就會(huì)導(dǎo)致災(zāi)難性后果。對(duì)于服務(wù)器端的程序而言,如果客戶端所需的某些受鎖保護(hù)的數(shù)據(jù)結(jié)構(gòu)需求量大增,則經(jīng)常會(huì)發(fā)生這種情況。
            例如,請(qǐng)?jiān)O(shè)想以下情況:平均來(lái)說(shuō),每 100 毫秒會(huì)到達(dá) 8 個(gè)請(qǐng)求。我們將八個(gè)線程用于服務(wù)請(qǐng)求(因?yàn)槲覀兪褂玫氖?8-CPU 計(jì)算機(jī))。這八個(gè)線程中的每一個(gè)都必須獲得一個(gè)鎖并保持 20 毫秒,然后才能展開(kāi)實(shí)質(zhì)的工作。
            遺憾的是,對(duì)這個(gè)鎖的訪問(wèn)需要進(jìn)行序列化處理,因此,全部八個(gè)線程需要 160 毫秒才能進(jìn)入并離開(kāi)鎖。第一個(gè)退出后,需要經(jīng)過(guò) 140 毫秒第九個(gè)線程才能訪問(wèn)該鎖。此方案本質(zhì)上無(wú)法進(jìn)行調(diào)整,因此備份的請(qǐng)求會(huì)不斷增長(zhǎng)。隨著時(shí)間的推移,如果到達(dá)率不降低,客戶端請(qǐng)求就會(huì)開(kāi)始超時(shí),進(jìn)而發(fā)生災(zāi)難性后果。
            眾所周知,在鎖中是通過(guò)公平性對(duì)鎖進(jìn)行保護(hù)的。原因在于在鎖本來(lái)已經(jīng)可用的時(shí)間段內(nèi),鎖被人為封閉,使得到達(dá)的線程必須等待,直到所選鎖的擁有者線程能夠喚醒、切換上下文以及獲得和釋放該鎖為止。為解決這種問(wèn)題,Windows 已逐漸將所有內(nèi)部鎖都改為不公平鎖,而且 CLR 監(jiān)視器也是不公平的。
            對(duì)于這種有關(guān)保護(hù)的基本問(wèn)題,唯一的有效解決方案是減少鎖持有時(shí)間并分解系統(tǒng)以盡可能減少熱鎖(如果有的話)。雖然說(shuō)起來(lái)容易做起來(lái)難,但這對(duì)于可伸縮性來(lái)說(shuō)還是非常重要的。

            “蜂擁”是指大量線程被喚醒,使得它們?nèi)客瑫r(shí)從 Windows 線程計(jì)劃程序爭(zhēng)奪關(guān)注點(diǎn)。例如,如果在單個(gè)手動(dòng)設(shè)置事件中有 100 個(gè)阻塞的線程,而您設(shè)置該事件…嗯,算了吧,您很可能會(huì)把事情弄得一團(tuán)糟,特別是當(dāng)其中的大部分線程都必須再次等待時(shí)。
            實(shí)現(xiàn)阻塞隊(duì)列的一種途徑是使用手動(dòng)設(shè)置事件,當(dāng)隊(duì)列為空時(shí)變?yōu)闊o(wú)信號(hào)而在隊(duì)列非空時(shí)變?yōu)橛行盘?hào)。遺憾的是,如果從零個(gè)元素過(guò)渡到一個(gè)元素時(shí)存在大量正在等待的線程,則可能會(huì)發(fā)生蜂擁。這是因?yàn)橹挥幸粋€(gè)線程會(huì)得到此單一元素,此過(guò)程會(huì)使隊(duì)列變空,從而必須重置該事件。如果有 100 個(gè)線程在等待,那么其中的 99 個(gè)將被喚醒、切換上下文(導(dǎo)致所有緩存丟失),所有這些換來(lái)的只是不得不再次等待。

            兩步舞曲
            有時(shí)您需要在持有鎖的情況下通知一個(gè)事件。如果喚醒的線程需要獲得被持有的鎖,則這可能會(huì)很不湊巧,因?yàn)樵谒粏拘押笾皇前l(fā)現(xiàn)了它必須再次等待。這樣做非常浪費(fèi)資源,而且會(huì)增加上下文切換的總數(shù)。此情況稱為兩步舞曲,如果涉及到許多鎖和事件,可能會(huì)遠(yuǎn)遠(yuǎn)超出兩步的范疇。
            Win32 和 CLR 的條件變量支持在本質(zhì)上都會(huì)遇到兩步舞曲問(wèn)題。它通常是不可避免的,或者很難解決。
            兩步舞曲問(wèn)題在單處理器計(jì)算機(jī)上情況更糟。在涉及到事件時(shí),內(nèi)核會(huì)將優(yōu)先級(jí)提升應(yīng)用到喚醒的線程。這幾乎可以保證搶先占用線程,使其能夠在有機(jī)會(huì)釋放鎖之前設(shè)置事件。這是在極端情況下的兩步舞曲,其中設(shè)置 ThreadA 已切換出上下文,使得喚醒的 ThreadB 可以嘗試獲得鎖;當(dāng)然它無(wú)法做到,因此它將進(jìn)行上下文切換以使 ThreadA 可再次運(yùn)行;最終,ThreadA 將釋放鎖,這將再次提升 ThreadB 的優(yōu)先級(jí),使其優(yōu)先于 ThreadA,以便它能夠運(yùn)行。如您所見(jiàn),這涉及了多次無(wú)用的上下文切換。

            優(yōu)先級(jí)反轉(zhuǎn)
            修改線程優(yōu)先級(jí)常常是自找苦吃。當(dāng)不同優(yōu)先級(jí)的許多線程共享對(duì)同樣的鎖和資源的訪問(wèn)權(quán)時(shí),可能會(huì)發(fā)生優(yōu)先級(jí)反轉(zhuǎn),即較低優(yōu)先級(jí)的線程實(shí)際無(wú)限期地阻止較高優(yōu)先級(jí)線程的進(jìn)度。這個(gè)示例所要說(shuō)明的道理就是盡可能避免更改線程優(yōu)先級(jí)。
            下面是一個(gè)優(yōu)先級(jí)反轉(zhuǎn)的極端示例。假設(shè)低優(yōu)先級(jí)的 ThreadA 獲得某個(gè)鎖 L。隨后高優(yōu)先級(jí)的 ThreadB 介入。它嘗試獲得 L,但由于 ThreadA 占用使得它無(wú)法獲得。下面就是“反轉(zhuǎn)”部分:好像 ThreadA 被人為臨時(shí)賦予了一個(gè)高于 ThreadB 的優(yōu)先級(jí),這一切只是因?yàn)樗钟?ThreadB 所需的鎖。
            當(dāng) ThreadA 釋放了鎖后,此情況最終會(huì)自行解決。遺憾的是,如果涉及到中等優(yōu)先級(jí)的 ThreadC,設(shè)想一下會(huì)發(fā)生什么情況。雖然 ThreadC 不需要鎖 L,但它的存在可能會(huì)從根本上阻止 ThreadA 運(yùn)行,這將間接地阻止高優(yōu)先級(jí) ThreadB 的運(yùn)行。
            最終,Windows Balance Set Manager 線程會(huì)注意到這一情況。即使 ThreadC 保持永遠(yuǎn)可運(yùn)行狀態(tài),ThreadA 最終(四秒鐘后)也將接收到操作系統(tǒng)發(fā)出的臨時(shí)優(yōu)先級(jí)提升指令。但愿這足以使其運(yùn)行完畢并釋放鎖。但這里的延遲(四秒鐘)相當(dāng)巨大,如果涉及到任何用戶界面,則應(yīng)用程序用戶肯定會(huì)注意到這一問(wèn)題。

            實(shí)現(xiàn)安全性的模式
            現(xiàn)在我已經(jīng)找出了一個(gè)又一個(gè)的問(wèn)題,好消息是我這里還有幾種設(shè)計(jì)模式,您可以遵循它們來(lái)降低上述問(wèn)題(尤其是正確性危險(xiǎn))的發(fā)生頻率。大多數(shù)問(wèn)題的關(guān)鍵是由于狀態(tài)在多個(gè)線程之間共享。更糟的是,此狀態(tài)可被隨意控制,可從一致?tīng)顟B(tài)轉(zhuǎn)換為不一致?tīng)顟B(tài),然后(但愿)又重新轉(zhuǎn)換回來(lái),具有令人驚訝的規(guī)律性。
            當(dāng)開(kāi)發(fā)人員針對(duì)單線程程序編寫(xiě)代碼時(shí),所有這些都非常有用。在您向最終的正確目標(biāo)邁進(jìn)的過(guò)程中,很可能會(huì)使用共享內(nèi)存作為一種暫存器。多年來(lái) C 語(yǔ)言風(fēng)格的命令式編程語(yǔ)言一直使用這種方式工作。
            但隨著并發(fā)現(xiàn)象越來(lái)越多,您需要對(duì)這些習(xí)慣密切加以關(guān)注。您可以按照 Haskell、LISP、Scheme、ML 甚至 F#(一種符合 .NET 的新語(yǔ)言)等函數(shù)式編程語(yǔ)言行事,即采用不變性、純度和隔離作為一類設(shè)計(jì)概念。

            不變性
            具有不變性的數(shù)據(jù)結(jié)構(gòu)是指在構(gòu)建后不會(huì)發(fā)生改變的結(jié)構(gòu)。這是并發(fā)程序的一種奇妙屬性,因?yàn)槿绻麛?shù)據(jù)不改變,則即使許多線程同時(shí)訪問(wèn)它也不會(huì)存在任何沖突風(fēng)險(xiǎn)。這意味著同步并不是一個(gè)需要考慮的因素。
            不變性在 C++ 中通過(guò) const 提供支持,在 C# 中通過(guò)只讀修飾符支持。例如,僅具有只讀字段的 .NET 類型是淺層不變的。默認(rèn)情況下,F(xiàn)# 會(huì)創(chuàng)建固定不變的類型,除非您使用可變修飾符。再進(jìn)一步,如果這些字段中的每個(gè)字段本身都指向字段均為只讀(并僅指向深層不可變類型)的另一種類型,則該類型是深層不可變的。這將產(chǎn)生一個(gè)保證不會(huì)改變的完整對(duì)象圖表,它會(huì)非常有用。
            所有這一切都說(shuō)明不變性是一個(gè)靜態(tài)屬性。按照慣例,對(duì)象也可以是固定不變的,即在某種程度上可以保證狀態(tài)在某個(gè)時(shí)間段不會(huì)改變。這是一種動(dòng)態(tài)屬性。Windows Presentation Foundation (WPF) 的可凍結(jié)功能恰好可實(shí)現(xiàn)這一點(diǎn),它還允許在不同步的情況下進(jìn)行并行訪問(wèn)(但是無(wú)法以處理靜態(tài)支持的方式對(duì)其進(jìn)行檢查)。對(duì)于在整個(gè)生存期內(nèi)需要在固定不變和可變之間進(jìn)行轉(zhuǎn)換的對(duì)象來(lái)說(shuō),動(dòng)態(tài)不變性通常非常有用。
            不變性也存在一些弊端。只要有內(nèi)容需要改變,就必須生成原始對(duì)象的副本并在此過(guò)程中應(yīng)用更改。另外,在對(duì)象圖表中通常無(wú)法進(jìn)行循環(huán)(除動(dòng)態(tài)不變性外)。
            例如,假設(shè)您有一個(gè) ImmutableStack<T>,如圖 4 所示。您需要從包含已應(yīng)用更改的對(duì)象中返回新的 ImmutableStack<T> 對(duì)象,而不是一組變化的 Push 和 Pop 方法。在某些情況下,可以靈活使用一些技巧(與堆棧一樣)在各實(shí)例之間共享內(nèi)存。
            public class ImmutableStack<T> {
                private readonly T m_value;
                private readonly ImmutableStack<T> m_next;
                private readonly bool m_empty;
                public ImmutableStack() { m_empty = true; }
                internal ImmutableStack(T value, Node next) {
                    m_value = value;
                    m_next = next;
                    m_empty = false;
                }
                public ImmutableStack<T> Push(T value) {
                    return new ImmutableStack(value, this);
                }
                public ImmutableStack<T> Pop(out T value) {
                    if (m_empty) throw new Exception("Empty.");
                    return m_next;
                }
            }
            
            節(jié)點(diǎn)被推入時(shí),必須為每個(gè)節(jié)點(diǎn)分配一個(gè)新對(duì)象。在堆棧的標(biāo)準(zhǔn)鏈接列表實(shí)現(xiàn)中,必須執(zhí)行此操作。但是要注意,當(dāng)您從堆棧中彈出元素時(shí),可以使用現(xiàn)有的對(duì)象。這是因?yàn)槎褩V械拿總€(gè)節(jié)點(diǎn)是固定不變的。
            固定不變的類型無(wú)處不在。CLR 的 System.String 類是固定不變的,還有一個(gè)設(shè)計(jì)指導(dǎo)原則,即所有新值類型都應(yīng)是固定不變的。此處給出的指導(dǎo)原則是在可行和合適的情況下使用不變性并抵抗執(zhí)行變化的誘惑,而最新一代的語(yǔ)言會(huì)使其變得非常方便。

            純度
            即使是使用固定不變的數(shù)據(jù)類型,程序所執(zhí)行的大部分操作仍是方法調(diào)用。方法調(diào)用可能存在一些副作用,它們?cè)诓l(fā)代碼中會(huì)引發(fā)問(wèn)題,因?yàn)楦弊饔靡馕吨撤N形式的變化。通常這只是表示寫(xiě)入共享內(nèi)存,但它也可能是實(shí)際變化的操作,如數(shù)據(jù)庫(kù)事務(wù)、Web 服務(wù)調(diào)用或文件系統(tǒng)操作。在許多情況下,我希望能夠調(diào)用某種方法,而又不必?fù)?dān)心它會(huì)導(dǎo)致并發(fā)危險(xiǎn)。有關(guān)這一方面的一些很好示例就是 GetHashCode 和 ToString on System.Object 等簡(jiǎn)單的方法。很多人都不希望它們帶來(lái)副作用。
            純方法始終都可以在并發(fā)設(shè)置中運(yùn)行,而無(wú)需添加同步。盡管純度沒(méi)有任何常見(jiàn)語(yǔ)言支持,但您可以非常簡(jiǎn)單地定義純方法:
            1. 它只從共享內(nèi)存讀取,并且只讀取不變狀態(tài)或常態(tài)。
            2. 它必須能夠?qū)懭刖植孔兞俊?
            3. 它可以只調(diào)用其他純方法。
            因此,純方法可以實(shí)現(xiàn)的功能非常有限。但當(dāng)與不變類型結(jié)合使用時(shí),純度就會(huì)成為可能而且非常方便。一些函數(shù)式語(yǔ)言默認(rèn)情況下都采用純度,特別是 Haskell,它的所有內(nèi)容都是純的。任何需要執(zhí)行副作用的內(nèi)容都必須封裝到一個(gè)被稱為 monad 的特殊內(nèi)容中。但是我們中的多數(shù)人都不使用 Haskell,因此我們必須遵照純度約定。

            隔離
            前面我們只是簡(jiǎn)單提及了發(fā)布和私有化,但它們卻擊中了一個(gè)非常重要的問(wèn)題的核心。由于狀態(tài)通常在多個(gè)線程之間共享,因此同步是必不可少的(不變性和純度也很有趣味)。但如果狀態(tài)被限制在單個(gè)線程內(nèi),則無(wú)需進(jìn)行同步。這會(huì)導(dǎo)致軟件在本質(zhì)上更具伸縮性。
            實(shí)際上,如果狀態(tài)是隔離的,則可以自由變化。這非常方便,因?yàn)樽兓谴蟛糠?C 風(fēng)格語(yǔ)言的基本內(nèi)置功能。程序員已習(xí)慣了這一點(diǎn)。這需要進(jìn)行訓(xùn)練以便能夠在編程時(shí)以函數(shù)式風(fēng)格為主,對(duì)大多數(shù)開(kāi)發(fā)人員來(lái)說(shuō)這都相當(dāng)困難。嘗試一下,但不要自欺欺人地認(rèn)為世界會(huì)在一夜之間改為使用函數(shù)式風(fēng)格編程。
            所有權(quán)是一件很難跟蹤的事情。對(duì)象是何時(shí)變?yōu)楣蚕淼模吭诔跏蓟瘯r(shí),這是由單線程完成的,對(duì)象本身還不能從其他線程訪問(wèn)。將對(duì)某個(gè)對(duì)象的引用存儲(chǔ)在靜態(tài)變量中、存儲(chǔ)在已在線程創(chuàng)建或排列隊(duì)列時(shí)共享的某個(gè)位置或存儲(chǔ)在可從其中的某個(gè)位置傳遞性訪問(wèn)的對(duì)象字段中之后,該對(duì)象就變?yōu)楣蚕韺?duì)象。開(kāi)發(fā)人員必須特別關(guān)注私有與共享之間的這些轉(zhuǎn)換,并小心處理所有共享狀態(tài)。

            Joe Duffy 在 Microsoft 是 .NET 并行擴(kuò)展方面的開(kāi)發(fā)主管。他的大部分時(shí)間都在攻擊代碼、監(jiān)督庫(kù)的設(shè)計(jì)以及管理夢(mèng)幻開(kāi)發(fā)團(tuán)隊(duì)。他的最新著作是《Concurrent Programming on Windows》
            思思久久99热只有频精品66| 狠狠色噜噜狠狠狠狠狠色综合久久| 青青草原综合久久| 国产欧美一区二区久久| 婷婷久久香蕉五月综合加勒比| 久久99热这里只有精品66| 久久久久久国产精品无码下载 | 日韩欧美亚洲综合久久影院d3| 久久中文骚妇内射| 久久精品亚洲日本波多野结衣| 久久综合视频网| 久久天天躁狠狠躁夜夜躁2014| 色青青草原桃花久久综合| 久久99国产精品久久99小说| 久久人妻AV中文字幕| 久久九九久精品国产免费直播| 久久综合亚洲鲁鲁五月天| 久久婷婷五月综合97色直播| 97精品依人久久久大香线蕉97| 久久久久国产精品人妻| 亚洲欧美日韩中文久久| 人妻无码久久一区二区三区免费| 日韩精品久久久久久久电影蜜臀| 国产午夜精品久久久久免费视| 久久99国产精品二区不卡| 久久美女网站免费| 久久精品免费大片国产大片| 亚洲伊人久久综合中文成人网 | 久久精品中文字幕一区| 99久久99久久精品国产片果冻| 久久国产精品一国产精品金尊| 九九久久99综合一区二区| 久久国产精品一区| 久久精品国产日本波多野结衣 | 久久天天躁狠狠躁夜夜avapp| 亚洲午夜久久久影院伊人| 国产成人精品免费久久久久| 91久久精品无码一区二区毛片| 久久久精品日本一区二区三区| 2020国产成人久久精品| 国产一区二区三区久久|