程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> C# event線程安全,

C# event線程安全,

編輯:C#入門知識

C# event線程安全,


突然想到有關C#中使用event特性時關於線程安全的問題,以前雖然有遵從“復制引用+null判斷”的模式(盲目地),但沒有深入了解和思考。

為之查詢了資料和實驗,對此有了進一步的理解。

 

一般event使用模式

定義(field-like event):

public event EventHandler Done;

類內raise:

protected void OnDone()
{
    var done = Done;
    if (done != null)
    {
        done(this, new EventArgs());
    }
}

不禁要問,為何要復制引用?多線程下表現如何?

 

關於C#3.0和C#4.0中編譯器對event實現的整理

為了解決上面哪些疑惑,我查了一些資料,其中有來自當時C#編譯器開發組成員的一篇博文 Field-like Events Considered Harmful。

這篇博文介紹了C#3.0中編譯器對於field-like event(也是最常見的使用方式)的實現。

 

對於如此的代碼,

class EventInCS3
{
    public event EventHandler Done;
}

編譯器會將其轉換成:

class EventInCS3
{
    private EventHandler __Done; // 1
    public event EventHandler Done
    {
        add
        {
            lock (this) // 2
            {
                __Done = __Done + value; // 3
            }
        }
        remove
        {
            lock (this) { __Done = __Done - value; }
        }
    }
}

有以下幾點值得注意(同注釋編號):

1.event下隱藏的真正delegate鏈。實際上我們使用的是子類MulticastDelegate(可以參考 開源的coreclr實現)。

3.正如+、-操作符對於string類型是起字符串組合作用,其對於delegate類型也同樣是起到兩條鏈的組合作用(參考 MSDN),實際上是調用了Delegate.Combine和Delegate.Remove。同時也引入了經典的線程問題(修改丟失)。

2.為了解決多線程問題,使用了lock。

(就先不管這個lock(this)了。當然上面提到的 博文 裡提到了,編譯器並不是通過lock,繼而通過Monitor的靜態方法來同步,而是通過IL即MethodImplAttribute(MethodImplOptions.Synchronized)實現。這些都是C#本身不推薦的方法。)

 

而在C#4.0中,同步的實現有了變化,同樣參見同一作者兩年後的 這一篇博文。

編譯器默認的add、remove實現,改為使用compare and swap來實現lock-free同步。值得注意的是,delegate是不可更改的類型,即+=、-=之後,會指向一個新的對象,而不再是原對象(類似string)。

通過IL查看程序集裡生成的add_Done、remove_Done,可以發現端倪,大致會生成如下的代碼:

static void add_Done(EventHandler value)
{
    EventHandler V_0 = __Done;
    EventHandler V_1, V_2;
    do
    {
        V_1 = V_0;
        V_2 = (EventHandler)Delegate.Combine(V_1, value);
        V_0 = Interlocked.CompareExchange<EventHandler>(ref __Done, V_2, V_1);
    } while (V_0 != V_1);
}

 

C#4.0中event相關的語義變化整理

在同一作者的 另一篇博文 中,介紹了C#4.0中event相關的語義變化,主要是+=、-=操作符的語義變化

 

在C#3.0中,對於一個event,如果在該類之外訪問這個event,則會被認為是訪問這個event本身,如我們熟知的只能通過+=、-=這兩個操作符來訪問(即是調用對應的add、remove訪問器);而在類的內部,所有對這個event的訪問,都會被認為是訪問作為event實現的delegate本身(即訪問Done,實際上訪問到的是__Done)。

這麼處理的話,我們就能在OnDone方法裡復制引用,判斷null,進行調用。因為此時Done這個標識符,代表的是一個EventHandler對象的引用。

C#3.0的問題也在於此,這種情況下,我們寫下

Done += SomeHandlerMethod;

時,+=實際是調用了:

EventHandler EventHandler.operator +(EventHandler left, EventHandler right)

在Visual Studio 2015裡寫一個普通的、非event的EventHandler的+=運算,鼠標放在+=上時,顯示的也是這個函數簽名。C#3.0時即使對event也是這麼處理的。

導致我們失去了默認add訪問器提供的同步功能

 

而這一現象在C#4.0中得到了改善。在類內部訪問event的標識符時,+=、-=操作符就會被認為是add、remove的調用了。

可知在C#4.0寫下同樣的代碼時,+=調用的簽名為:

void EventInCS4.Done.add 

 

自定義event訪問器

自定義event時(非field-like event),我們自己編寫的add、remove訪問器就沒有默認的同步了。如果要考慮線程安全,需要手動加上同步(比如lock(someLockObject))。

此時,在類內部訪問event標識符,只會被當成是訪問event本身。要引發事件(Done)的話,需訪問對應delegate(__Done(this, new EventArgs()))。

 

操作event的正確方式

一般情況下無需自己實現event,用field-like就好了。

因為不管是通過event標識符訪問delegate(field-like event),還是直接訪問delegate(自定義event),我們得到的都是delegate對象的引用,而且delegate對象是不可更改的。引用的復制是原子的。所以我們可以隨意地復制該delegate的引用,然後判斷null並invoke。

 

一些code snippet如:

 

對於編譯器是否會將復制引用作為重復的局部變量優化掉,以至於在一些情況下需要使用諸如以下的方式的問題,我沒有深入了解。

Interlocked.CompareExchange(ref Done, null, null);

簡單查詢一下之後,得知對於微軟自家的CLR無需關心這個問題,蓋其遵循較嚴格的內存模型(memory model),不會引入新的讀取操作。但其他情況下有可能存在這樣的問題。相關文章和討論鏈接如下:

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved