解析 XPCOM 物件的 Reference Count

在 Gecko 的實作裡,我們大量使用 smart pointer 作為指標傳遞間的媒介,而為了能完善的操作 reference count ,我們必須保證 reference count 的增減是 thread safe 的。若無法妥善處理 reference count 的增減而產生 race condition ,會造成至少以下兩個問題:

  1. 兩條以上的 thread 同時執行了 delete pointer 的動作,造成了 double free ,程式 crash
  2. 兩條以上的 thread 同時執行 Release 的動作,但卻同時判斷 reference count 為非0,導致該有的 delete 並沒有如預期的去執行,造成 memory leak

以下會一步步針對 Gecko 中 XPCOM 物件的 reference count 如何確保 thread safe 來作解析。以常見的Smart Pointer實作上,大致分為兩種:

  1. reference count 是 smart pointer 本身的 class 負責控管,在 constructor/destructor 作 reference count 的增減
  2. reference count 是綁在每個 object 上,而 smart pointer 本身 class 只是在建構解構時,請物件對自 己的reference count作增減

兩者各有利弊, 像常見的 std::shared_ptr 就是第一種設計方式,而 Mozilla 的實作則是將 reference count 長在每一個 XPCOM 的物件上。

而要成為 Gecko 中 smart pointer 能夠操作的物件,要滿足以下幾點:

  1. 不能有公開的解構子 destructor 。如果一個物件是受到 smart pointer 控管生命週期,我們不允許在 class scope 外可以 explicit 利用 operator delete 操作物件。除了跟使用smart pointer 做reference counted 的本意牴觸外,也會有額外的濳在問題發生,例如 smart pointer 操作了已經 deleted 的物件
  2. 需要實作 AddRef(void) 和 Release(void) 這兩組界面

解析 Gecko 的實作

在 Gecko 中 , nsISupportsImpl.h 有提供許多 MACRO 好讓開發者能夠直接在 XPCOM class 內產生滿足上面的條件的程式碼,以下已兩個常見的 MACRO 作為例子

這兩個 MACRO 唯一的差異只有在 mRefCnt 的型態不一樣,這是因為 XPCOM 物件在設計的時候必須考慮到執行環境是在 single-thread 還是 multi-thread 。若是你自認只有單一 thread 會去操作這物件,就要優先考量使用非 THREADSAFE 版本的實作方式。

來分析這兩種 reference count 的差異,

基本上這兩者的實作幾乎一模一樣,同樣都提供了 prefix 版本的 operator++ 和 – – 來針對內部的 mValue 作加減,也都提供了 cast operator 來讓外部可以判別 mValue 是否等於0,差別就只有在 mValue 的型別,以及 isThreadSafe 這 flag 在 ThreadSafeAutoRefCnt 裡會設定為 true 。nsAutoRefCnt::mValue 的型別其實就只是個透過 typedef 出來的 primitive type ,因為最初的假設就是 single thread 會使用這物件,並不會有 race condition 發生的可能性,所以只是非常單純的對數值做加減,不須特別對 reference count 作特別保護,以減少不必要的 overhead 。然而 ThreadSafeAutoRefCnt::mValue 則是使用 mfbt 所wrap過的 Atomic class ,來確保操作 mValue 是一個 atomic operation 。而 mfbt 的實作則是封裝了 c++11 所提供的 std::atomic

剛剛在 NS_DECL_ISUPPORTS 和 NS_DECL_THREADSAFE_ISUPPORTS 的 MACRO 中,我們會在 Debug Build 或是 Nightly Build 中偷偷宣告一個物件 nsAutoOwningThread _mOwningThread; 他的實作相當簡單,只是透過 PR_GetCurrentThread 這 function 透過系統API得到 current thread 的 handle 紀錄在 mThread,之後會介紹他的用途。

接下來我們來看一下 AddRef 和 Release 兩者的實作,不管是否 thread safe ,實作方式都一樣透過 NS_IMPL_ADDREF(_class)NS_IMPL_RELEASE(_class) 來達成,

首先先看第一個 MACRO MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING

這邊就是一開始提到的 XPCOM 物件不能有公開的解構子,利用 static_assert 在 compile time 做檢查,若 HasDangerousPublicDestructor::value 沒特別作手腳則預設是 false ,若真的有 public destructor 的需求,請針對 HasDangerousPublicDestructor 作特化 ,但這種需求再 Gecko code 裡面非常的少。要怎麼判斷一個 class 是否含有 public 解構子呢? Gecko 這邊有針對 compiler 版本作檢查,如果有支援,則直接使用 std::is_destructible 標準函式庫來作判斷,若不支援, Gecko 也有自己實作的版本

這部份最關鍵的地方是用神妙的 template deduction 透過這行
template<typename T, typename = decltype(Declval<T>().~T())> 顯式呼叫解構子,在 compile time 推導出是否含有 public 解構子,達成確保 XPCOM 物件中沒有 expose public destructor 的條件限制。

回到剛剛 AddRef 的實作,

假如 isThreadSafe 是 false 也就是說我們預期我們的 XPCOM 物件是執行在 single thread 上,會針對這個前提作一個 asssert 來確保這個假設是成立而沒有漏考慮的情況 NS_ASSERT_OWNINGTHREAD(_class) 展開後會是

很明顯可以看出這邊判斷了一個我們一開始生成 XPCOM 物件所偷偷紀錄的 thread handle 是否跟目前所在的 thread handle 相同?如果不一樣則觸發 crash ,讓開發者好在 Debug build 或是 Nightly build 的時候能夠明確知道有與設計上衝突的情況發生,讓開發者能夠及早發現及早修正。反之,若是THREADSAFE版本的 XPCOM Object 則不需要執行這個判斷。接下來就是直接執行 nsrefcnt count = ++mRefCnt; 再將reference count return。

看完了 AddRef 後,Release 的實作也都大同小異

當 reference count 歸零時,會去執行帶入的 _destroy,若沒特別指定就是帶入 delete (this) ,完美的處理記憶體的回收。


結語

當你在設計自己的 XPCOM Component 時,必須先考慮到這個 Component 是否會在多條 thread 間傳遞,如果有這種可能,就必須用 NS_DECL_THREADSAFE_ISUPPORTS ,否則就應該直接使用 NS_DECL_ISUPPORTS 。
如果原先預期是在 single thread 而不小心在不同 thread 使用了也不用擔心,在傳遞 smart pointer 時都會經過 AddRef 和 Release,上面提到的 assert 都會在 debug stage 就讓你知道,讓你可以趕緊分析問題並且修正。
這邊必須提醒的是,上述保護的只有 reference count 並沒有做任何 member function 內的thread synchronization 保護,還是必須要針對 function 內部如果有 critical section 的情形,透過Gecko提供的 mutex, monitor …等 synchronized 物件來確保 thread safe 。

在 Gecko 中,也有非XPCOM的物件,因為也想使用 smart pointer 來操控,也會透過 MACRO 來實作 AddRef 和 Release ,實際的實作內容也都大同小異在這邊就不多作解釋了。

您可能也會喜歡

目前找不到相關文章

共 1 則讀者回應

  1. 請問我有機會認識您嗎? 從字裡行間感覺初你天真自然不做作的性格 請問有機會向您討教XPCOM 相關的問題嗎? 謝謝

對此文章發表回應

你的電子郵件位址並不會被公開。 必要欄位標記為 *