.NET(C#):再議實值型別 – 原子性

來源:互聯網
上載者:User

兩年前我曾經寫過一篇講理解實值型別,參考型別的文章,主要是講常見的實值型別,參考型別的區別。但是這兩種類型的淵源不止那些,今天就說說實值型別線上程方面的原子性問題。

目錄

  • 0. 概念闡述
  • 1. 使用Interlocked類型
  • 2. 使用參考型別來聲明
  • 3. 通過裝箱和拆箱
  • 4. 使用lock

 

返回目錄
0. 概念闡述

首先我想專門強調一下,“原子性”和“安全執行緒”屬於容易混淆的概念,解決了“原子性”不等於解決了“安全執行緒”,但是如果連“原子性”都沒有保證,那麼肯定不是“安全執行緒”的。“原子性”屬於“安全執行緒”考慮的一個範疇,本文也僅僅是討論“原子性”問題。

 

根據C#語言標準的描述,只有如下類型的讀寫是保證具有原子性的:

bool, char, byte, sbyte, short, ushort, uint, int, float,參考型別,和以上述實值型別做為底層類型的枚舉類型。

但同時注意這些類型的運算(包含遞增++和遞減--)不是原子性的。

 

但是根據CLI標準。

I.12.6.6(Atomic reads and writes):CLI應該保證如下類型的讀寫是原子性的:

在記憶體中正確對其的位置,且大小不大於CPU一次性處理的字大小(也就是所謂的WORD大小,等於本地int大小)。

 

那麼可以推理出在64位CPU的環境下,64位的內建類型(如long, ulong, double)的讀寫也是具有原子性的。這一點在Eric Lippert的部落格中敘述過。不過注意,Eric Lippert也在其文章中提到由於這是CLI的規定,而不是C#語言的規定,因此某些非標準CLI執行環境可能會打破這個推斷。因此對於這些特殊類型,我個人覺得最好還是用Interlocked類型提供的操作,或者使用lock。下文會詳細講這些內容。

 

下面開始闡述嘗試解決實值型別原子性的各種方法,越是上面的方法越推薦使用。

 

返回目錄
1. 使用Interlocked類型

對於解決實值型別的原子性問題,首先考慮的類型就是System.Threading.Interlocked類型。如,Interlocked類型的方法成員:

可以看到,Interlocked類型提供多種形式的原子性操作:讀取,寫入,遞增,遞減等。同時還支援多種類型。

 

使用Interlocked類型提供的方法可以減少lock的使用從而增加效能,比如實現一個計數器的添加原子性操作,其實是沒有必要像這樣使用lock的:

readonly object locker = new object();

 

int myVar;

//執行原子性增值操作的方法

public void AtomicIncrement(int increment)

{

    lock (locker)

    {

        myVar += increment;

    }

}

 

完全可以使用Interlocked類型來執行這個原子性操作,使用如下更推薦的代碼:

int myVar;

//執行原子性增值操作的方法

public void AtomicIncrement(int increment)

{

    Interlocked.Add(ref myVar, increment);

}

這不僅會增加效能,還減少了代碼,不需要聲明用於lock的對象。

 

因此,如果能用到Interlocked的話,盡量就用Interlocked類型去解決問題,當然也肯定會有Interlocked類型不支援的方法,比如Read方法只支援long參數,沒有double。(這個問題之後會被解決的)

 

返回目錄
2. 使用參考型別來聲明

比如這樣一個實值型別:

struct MyStruct

{

    public long Data1;

    public double Data2;

}

 

顯然,該類型的對象佔用空間肯定是大於等於128位的,無論在32位還是64位CPU環境下,該類型的讀寫操作都不具備原子性的。而且Interlocked類型不支援使用者定義的結構體,那麼怎樣使其具備原子性呢?

其實如果可以的話,這裡最好的方法就是把它改成class,是的,參考型別的讀寫始終是具備原子性的。這樣的話我們仍然可以不用lock。

//把MyStruct改成MyClass,該類型的讀寫具備了原子性

class MyClass

{

    public long Data1;

    public double Data2;

}

 

返回目錄
3. 通過裝箱和拆箱

假設我們無法把上面的MyStruct結構體定義成參考型別,怎樣解決原子性讀寫的問題?還有一個小詭計的,先來描述一下問題,我們先聲明一個MyStruct結構體的屬性:

public MyStruct MyStructObject { get; set; }

 

顯然,MyStruct結構體的讀寫是不具備原子性的,如果一個線程在寫入MyStructObject屬性,而另一個線程在讀取MyStructObject變數,那麼可能會輸出意想不到的值,因為有可能在一個線程寫入了一半的時候,另一個線程開始了讀取操作。於是另一個線程通過MyStructObject屬性讀取的值可能既不是寫入前,也不是寫入後的值,而是部分寫入部分原始的不完整狀態。

//一個線程在寫入

MyStructObject = new MyStruct() { Data1 = 2, Data2 = 3 };

 

//另一個線程在讀取

//輸出既不是寫入前,也不是寫入後的值,而是部分寫入部分原始的不完整狀態!

Console.WriteLine("{0} {1}", MyStructObject.Data1, MyStructObject.Data2);

 

詭計就是可以使用裝箱和拆箱,把屬性聲明成object,如下:

//宣告類型為object

//對於MyStruct對象的讀寫需要裝箱和拆箱

public static object MyStructObject { get; set; }

 

這樣如果再次遇到上面的多線程讀寫代碼,不會有問題發生(注意讀取MyStructObject屬性時需要一次強制轉換,也就是裝箱操作),因為此時讀寫操作會進行裝箱拆箱操作,最終操作的是object引用對象,它的讀寫操作是具備原子性的。

 

返回目錄
4. 使用lock

上面提到過的方法都有自己的優勢,但是全部不是萬能的,而如果使用lock的話,雖然有額外的效能損耗,但它確是萬能的。所以如果有任何非原子性操作,可以把他嵌套到一個lock中,這樣其他線程就不會打斷這些操作,整個過程具備原子性了!

比如在32位CPU環境下,進行原子的double加法操作,可以這樣:

readonly object locker = new object();

 

double myDouble;

//執行原子性增值操作的方法

public void AtomicIncrement(double increment)

{

    //Interlocked.Add方法不支援double類型!!!

    lock (locker)

    {

        myDouble += increment;

    }

}

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.