.NET中保證線程安全的高級(jí)方法Interlocked類使用介紹
說(shuō)到線程安全,不要一下子就想到加鎖,尤其是可能會(huì)調(diào)用頻繁或者是要求高性能的場(chǎng)合。
對(duì)于性能要求不高或者同步的對(duì)象數(shù)量不多的時(shí)候,加鎖是一個(gè)比較簡(jiǎn)單而且易于實(shí)現(xiàn)的選擇。比方說(shuō).NET提供的一些基礎(chǔ)類庫(kù),比如線程安全的堆棧和隊(duì)列,如果使用加鎖的方式那么會(huì)使性能大打折扣(速度可能會(huì)降低好幾個(gè)數(shù)量級(jí)),而且如果設(shè)計(jì)得不好的話還有可能發(fā)生死鎖。
現(xiàn)在通過(guò)查看微軟的源代碼來(lái)學(xué)習(xí)一些不直接lock(等價(jià)于Monitor類)的線程同步技巧吧。
這里我們主要用的是Interlocked類,這個(gè)類按照M$的描述,是“為多個(gè)線程共享的變量提供原子操作”,當(dāng)然這個(gè)類是一個(gè)靜態(tài)類。這個(gè)類的源代碼看不到,因?yàn)槭钦{(diào)用的CLR內(nèi)部的方法,不過(guò)基本思想應(yīng)該是通過(guò)硬件原語(yǔ)try and set來(lái)實(shí)現(xiàn)的。
該類提供的Add、Increment、Decrement能夠完成簡(jiǎn)單的原子操作。
假如我們要提供一個(gè)計(jì)數(shù)器,每訪問(wèn)一次就遞增地返回一個(gè)新的數(shù)值用于計(jì)數(shù)。在多線程環(huán)境下,s++這一條語(yǔ)句不是線程安全的。因?yàn)閳?zhí)行這個(gè)語(yǔ)句要經(jīng)過(guò):移到寄存器(讀?。?、運(yùn)算、寫入這幾個(gè)步驟,在任何時(shí)候都可能會(huì)切換到其他線程,這樣子s被多個(gè)線程訪問(wèn)值可能會(huì)在切換的過(guò)程中丟失。有了Interlocked提供的這幾個(gè)原子操作的方法,就不用自己去加鎖實(shí)現(xiàn)這些簡(jiǎn)單的運(yùn)算了。由于是使用的硬件原語(yǔ),其效率自然也比加鎖高得多。
但是大多數(shù)情況下,問(wèn)題并沒(méi)有執(zhí)行相加相減運(yùn)算那么簡(jiǎn)單,這時(shí)如果不想用鎖的話就要想想辦法了。
以微軟的ConcurrentStack提供的線程安全的堆棧為例,分析一下如何實(shí)現(xiàn)如果往棧頭添加數(shù)據(jù)。
m_head是指向堆頂?shù)闹羔?,在定義的時(shí)候由于是多線程訪問(wèn)的,所以要加上volatile修飾符:
private volatile Node m_head;
如果是單線程的,那么入棧語(yǔ)句就是下面這個(gè)樣子:
1. Node newNode = new Node(item);
2. newNode.m_next = m_head;
3. m_head = newNode;
假如有兩個(gè)線程并發(fā)訪問(wèn)入棧方法的話,那么可能會(huì)產(chǎn)生如下情況:第一個(gè)線程執(zhí)行完第二條語(yǔ)句被打斷,第二個(gè)線程執(zhí)行到第二條語(yǔ)句又切換回第一個(gè)線程,兩個(gè)線程執(zhí)行完后有一個(gè)入棧的元素就不見(jiàn)了。那么如何實(shí)現(xiàn)線程安全呢?M$的代碼是這樣寫的:
Node newNode = new Node(item);
newNode.m_next = m_head;
if (Interlocked.CompareExchange(ref m_head, newNode, newNode.m_next) == newNode.m_next)
{
return;
}
// If we failed, go to the slow path and loop around until we succeed.
PushCore(newNode, newNode);
首先,Interlocked.CompareExchange比較兩個(gè)元素是否相等,并根據(jù)比較的結(jié)果替換其中一個(gè)元素,返回結(jié)果始終是第一個(gè)元素的原值。這個(gè)方法是原子操作。
那么這段代碼首先設(shè)置newNode的下一節(jié)點(diǎn)為堆棧頂部的元素,接下來(lái)CompareExchange,判斷棧頂元素有沒(méi)有被修改過(guò)。假如此時(shí)沒(méi)有另一個(gè)線程修改棧頂元素,那么m_head還是原來(lái)的值(上一條語(yǔ)句設(shè)置的新棧頂?shù)南乱粋€(gè)元素),此時(shí)就可以安全地把棧頂指針指向新元素,操作完成(return)。注意CompareExchange是原子操作的,所以在這期間棧頂元素不可能再被修改。
如果比較結(jié)果不相等,那么說(shuō)明棧頂元素已經(jīng)被其他線程修改了(此時(shí)返回值就是被修改后的棧頂,和上一條語(yǔ)句設(shè)置m_next不一樣),這樣CompareExchange就不會(huì)修改m_head,說(shuō)明入棧不成功,執(zhí)行PushCore方法。
這個(gè)東東的代碼如下:
private void PushCore(Node head, Node tail)
{
SpinWait spin = new SpinWait();
// Keep trying to CAS the exising head with the new node until we succeed.
do
{
spin.SpinOnce();
// Reread the head and link our new node.
tail.m_next = m_head;
}
while (Interlocked.CompareExchange(
ref m_head, head, tail.m_next) != tail.m_next);
#if !FEATURE_PAL && !FEATURE_CORECLR
if (CDSCollectionETWBCLProvider.Log.IsEnabled())
{
CDSCollectionETWBCLProvider.Log.ConcurrentStack_FastPushFailed(spin.Count);
}
#endif //!FEATURE_PAL && !FEATURE_CORECLR
}
可以看到其邏輯還是和上面那個(gè)一樣,只是加了一個(gè)循環(huán)直到操作完成。在這期間使用了一個(gè)SpinWait對(duì)象和SpinOnce方法,那么我們又要了解一下這是干嘛的。
關(guān)于SpinWait對(duì)象,M$的說(shuō)明是:System.Threading.SpinWait 是一個(gè)輕量同步類型,可以在低級(jí)別方案中使用它來(lái)避免內(nèi)核事件所需的高開(kāi)銷的上下文切換和內(nèi)核轉(zhuǎn)換。
關(guān)于它的說(shuō)明還有一堆,你可以參考這里。如果不想看那么多,那么只需了解它的使用場(chǎng)合是在資源不會(huì)被占用很長(zhǎng)時(shí)間的時(shí)候進(jìn)行等待,以用戶模式自旋以避免高額的開(kāi)銷。
SpinOnce的說(shuō)明很簡(jiǎn)單,就是執(zhí)行單一自旋,可以理解為等待一個(gè)很短的時(shí)間??偟膩?lái)說(shuō),當(dāng)自旋此時(shí)達(dá)到5次時(shí),會(huì)切換到同一處理器上的另一個(gè)線程,當(dāng)達(dá)到20次時(shí),會(huì)調(diào)用Thread的Sleep方法阻塞當(dāng)前線程,此時(shí)可以切換到其他同優(yōu)先級(jí)或更高優(yōu)先級(jí)的線程上去。
這樣,就可以避免加鎖(lock free)的高昂代價(jià)來(lái)實(shí)現(xiàn)線程的同步。
但是有的時(shí)候我們不能保證線程安全。比如堆棧的Count屬性,在程序調(diào)用這個(gè)屬性后,我們并不能保證這個(gè)屬性返回的時(shí)候是正確的,在返回到應(yīng)用程序的線程前元素?cái)?shù)量是有可能變化的,因此我們也就只能保證我們的返回值曾經(jīng)正確。
不過(guò)顯而易見(jiàn),這是可以接受的。對(duì)于開(kāi)發(fā)者來(lái)說(shuō),假如我們要訪問(wèn)一個(gè)多線程字典(ConcurrentDictionary)中的指定元素,我們不應(yīng)該是先判斷是否為空再取元素(因?yàn)樵乜赡茉谶@兩步操作之間被刪掉),而是應(yīng)該使用TryGetValue這種保證線程安全的方法來(lái)進(jìn)行操作。
相關(guān)文章
C#區(qū)分中英文按照指定長(zhǎng)度截取字符串的方法
這篇文章主要介紹了C#區(qū)分中英文按照指定長(zhǎng)度截取字符串的方法,涉及C#操作字符串的正則匹配與截取等常用操作技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-03-03c# xml轉(zhuǎn)word的實(shí)現(xiàn)示例
這篇文章主要介紹了c# xml轉(zhuǎn)word的實(shí)現(xiàn)示例,幫助大家更好的理解和學(xué)習(xí)使用c#,感興趣的朋友可以了解下2021-04-04C# Oracle批量插入數(shù)據(jù)進(jìn)度條的實(shí)現(xiàn)代碼
這篇文章主要介紹了C# Oracle批量插入數(shù)據(jù)進(jìn)度條的實(shí)現(xiàn)代碼,需要的朋友可以參考下2018-04-04C#實(shí)現(xiàn)的滾動(dòng)網(wǎng)頁(yè)截圖功能示例
這篇文章主要介紹了C#實(shí)現(xiàn)的滾動(dòng)網(wǎng)頁(yè)截圖功能,結(jié)合具體實(shí)例形式分析了C#圖形操作的相關(guān)技巧,需要的朋友可以參考下2017-07-07C#基于SerialPort類實(shí)現(xiàn)串口通訊詳解
這篇文章主要為大家詳細(xì)介紹了C#基于SerialPort類實(shí)現(xiàn)串口通訊,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01