突然想到有關c#中使用event特性時關於執行緒安全的問題,以前雖然有遵從「複製引用+null判斷」的模式(盲目地),但沒有深入了解和思考。
為之查詢了資料和實驗,對此有了進一步的理解。
定義(field-like event):
publicevent eventhandler done;
類內raise:
protectedvoid
ondone()
}
不禁要問,為何要複製引用?多執行緒下表現如何?
為了解決上面哪些疑惑,我查了一些資料,其中有來自當時c#編譯器開發組成員的一篇博文 field-like events considered harmful。
這篇博文介紹了c#3.0中編譯器對於field-like event(也是最常見的使用方式)的實現。
對於如此的**,
classeventincs3
編譯器會將其轉換成:
classeventincs3
}remove}}
}
有以下幾點值得注意(同注釋編號):
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,可以發現端倪,大致會生成如下的**:
staticvoid
add_done(eventhandler value)
while (v_0 !=v_1);
}
在同一作者的 另一篇博文 中,介紹了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時(非field-like event),我們自己編寫的add、remove訪問器就沒有預設的同步了。如果要考慮執行緒安全,需要手動加上同步(比如lock(somelockobject))。
此時,在類內部訪問event識別符號,只會被當成是訪問event本身。要引發事件(done)的話,需訪問對應delegate(__done(this, new eventargs()))。
一般情況下無需自己實現event,用field-like就好了。
因為不管是通過event識別符號訪問delegate(field-like event),還是直接訪問delegate(自定義event),我們得到的都是delegate物件的引用,而且delegate物件是不可更改的。引用的複製是原子的。所以我們可以隨意地複製該delegate的引用,然後判斷null並invoke。
一些code snippet如:
通過擴充套件方法來引發事件:
publicstatic
class
eventextension
}public
static
void raise(this eventhandler handler, object sender, eventargs args); //
過載版}
delegate的引用會以pass-by-value形式得到複製,所以直接
done.raise(this, new eventargs());
通過c#6.0提供的null-conditional操作符:
done?.invoke(this, new eventargs());
null-conditional操作符也會進行引用的複製,所以是執行緒安全的。(沒有done?(...)這種寫法)
對於編譯器是否會將複製引用作為重複的區域性變數優化掉,以至於在一些情況下需要使用諸如以下的方式的問題,我沒有深入了解。
interlocked.compareexchange(ref done, null, null);
MFC 執行緒同步 CEvent類
cevent類 cevent類提供了對事件的支援。事件是乙個允許乙個執行緒在某種情況發生時,喚醒另外乙個執行緒的同步物件。事件告訴執行緒何時去執行某乙個給定的任務,從而使多個執行緒流平滑。例如在某些網路應用程式中,乙個執行緒 記為a 負責監聽通訊埠,另乙個執行緒 記為b 負責更新使用者資料。通過使用...
CEvent事件跟執行緒的使用
建立乙個基於對話方塊的mfc工程,窗體上房乙個按鈕,建立乙個cbutton物件,在標頭檔案中加入cevent事件的標頭檔案 include 和執行緒標頭檔案 include 在對話方塊工程頭 h 檔案中宣告乙個執行緒處理函式 類外宣告 uint winapi threandone lpvoid pp...
事件CEvent的使用
cevent類的乙個物件,表示乙個 事件 乙個允許乙個事件發生時執行緒通知另乙個執行緒的同步物件。在乙個執行緒需要了解何時執行任務時,事件是十分有用的。例如,拷貝資料到資料文件時,執行緒應被通知何時資料是可用的。當新資料可用時,通過運用cevent物件來通知拷貝執行緒,執行緒才可能盡快地執行。例如在...