在 Gecko 的實作裡,我們大量使用 smart pointer 作為指標傳遞間的媒介,而為了能完善的操作 reference count ,我們必須保證 reference count 的增減是 thread safe 的。若無法妥善處理 reference count 的增減而產生 race condition ,會造成至少以下兩個問題:
- 兩條以上的 thread 同時執行了 delete pointer 的動作,造成了 double free ,程式 crash
- 兩條以上的 thread 同時執行 Release 的動作,但卻同時判斷 reference count 為非0,導致該有的 delete 並沒有如預期的去執行,造成 memory leak
以下會一步步針對 Gecko 中 XPCOM 物件的 reference count 如何確保 thread safe 來作解析。以常見的Smart Pointer實作上,大致分為兩種:
- reference count 是 smart pointer 本身的 class 負責控管,在 constructor/destructor 作 reference count 的增減
- reference count 是綁在每個 object 上,而 smart pointer 本身 class 只是在建構解構時,請物件對自 己的reference count作增減
兩者各有利弊, 像常見的 std::shared_ptr 就是第一種設計方式,而 Mozilla 的實作則是將 reference count 長在每一個 XPCOM 的物件上。
而要成為 Gecko 中 smart pointer 能夠操作的物件,要滿足以下幾點:
- 不能有公開的解構子 destructor 。如果一個物件是受到 smart pointer 控管生命週期,我們不允許在 class scope 外可以 explicit 利用 operator delete 操作物件。除了跟使用smart pointer 做reference counted 的本意牴觸外,也會有額外的濳在問題發生,例如 smart pointer 操作了已經 deleted 的物件
- 需要實作 AddRef(void) 和 Release(void) 這兩組界面
解析 Gecko 的實作
在 Gecko 中 , nsISupportsImpl.h 有提供許多 MACRO 好讓開發者能夠直接在 XPCOM class 內產生滿足上面的條件的程式碼,以下已兩個常見的 MACRO 作為例子
#define NS_DECL_ISUPPORTS \
public: \
NS_IMETHOD QueryInterface(REFNSIID aIID, \
void** aInstancePtr) override; \
NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override; \
NS_IMETHOD_(MozExternalRefCountType) Release(void) override; \
protected: \
nsAutoRefCnt mRefCnt; \
nsAutoOwningThread _mOwningThread; \
123456789
#define NS_DECL_ISUPPORTS \public: \ NS_IMETHOD QueryInterface(REFNSIID aIID, \ void** aInstancePtr) override; \ NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override; \ NS_IMETHOD_(MozExternalRefCountType) Release(void) override; \protected: \ nsAutoRefCnt mRefCnt; \ nsAutoOwningThread _mOwningThread; \
#define NS_DECL_THREADSAFE_ISUPPORTS \
public: \
NS_IMETHOD QueryInterface(REFNSIID aIID, \
void** aInstancePtr) override; \
NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override; \
NS_IMETHOD_(MozExternalRefCountType) Release(void) override; \
protected: \
::mozilla::ThreadSafeAutoRefCnt mRefCnt; \
nsAutoOwningThread _mOwningThread; \
123456789
#define NS_DECL_THREADSAFE_ISUPPORTS \public: \ NS_IMETHOD QueryInterface(REFNSIID aIID, \ void** aInstancePtr) override; \ NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override; \ NS_IMETHOD_(MozExternalRefCountType) Release(void) override; \protected: \ ::mozilla::ThreadSafeAutoRefCnt mRefCnt; \ nsAutoOwningThread _mOwningThread; \
這兩個 MACRO 唯一的差異只有在 mRefCnt 的型態不一樣,這是因為 XPCOM 物件在設計的時候必須考慮到執行環境是在 single-thread 還是 multi-thread 。若是你自認只有單一 thread 會去操作這物件,就要優先考量使用非 THREADSAFE 版本的實作方式。
來分析這兩種 reference count 的差異,
class nsAutoRefCnt
{
public:
nsAutoRefCnt() : mValue(0) {}
explicit nsAutoRefCnt(nsrefcnt aValue) : mValue(aValue) {}
// only support prefix increment/decrement
nsrefcnt operator++() { return ++mValue; }
nsrefcnt operator--() { return --mValue; }
nsrefcnt operator=(nsrefcnt aValue) { return (mValue = aValue); }
operator nsrefcnt() const { return mValue; }
nsrefcnt get() const { return mValue; }
static const bool isThreadSafe = false;
private:
nsrefcnt operator++(int) = delete;
nsrefcnt operator--(int) = delete;
nsrefcnt mValue;
};
1234567891011121314151617181920
class nsAutoRefCnt{public: nsAutoRefCnt() : mValue(0) {} explicit nsAutoRefCnt(nsrefcnt aValue) : mValue(aValue) {} // only support prefix increment/decrement nsrefcnt operator++() { return ++mValue; } nsrefcnt operator--() { return --mValue; } nsrefcnt operator=(nsrefcnt aValue) { return (mValue = aValue); } operator nsrefcnt() const { return mValue; } nsrefcnt get() const { return mValue; } static const bool isThreadSafe = false;private: nsrefcnt operator++(int) = delete; nsrefcnt operator--(int) = delete; nsrefcnt mValue;};
class ThreadSafeAutoRefCnt
{
public:
ThreadSafeAutoRefCnt() : mValue(0) {}
explicit ThreadSafeAutoRefCnt(nsrefcnt aValue) : mValue(aValue) {}
// only support prefix increment/decrement
MOZ_ALWAYS_INLINE nsrefcnt operator++() { return ++mValue; }
MOZ_ALWAYS_INLINE nsrefcnt operator--() { return --mValue; }
MOZ_ALWAYS_INLINE nsrefcnt operator=(nsrefcnt aValue)
{
return (mValue = aValue);
}
MOZ_ALWAYS_INLINE operator nsrefcnt() const { return mValue; }
MOZ_ALWAYS_INLINE nsrefcnt get() const { return mValue; }
static const bool isThreadSafe = true;
private:
nsrefcnt operator++(int) = delete;
nsrefcnt operator--(int) = delete;
// In theory, RelaseAcquire consistency (but no weaker) is sufficient for
// the counter. Making it weaker could speed up builds on ARM (but not x86),
// but could break pre-existing code that assumes sequential consistency.
Atomic<nsrefcnt> mValue;
};
1234567891011121314151617181920212223242526
class ThreadSafeAutoRefCnt{public: ThreadSafeAutoRefCnt() : mValue(0) {} explicit ThreadSafeAutoRefCnt(nsrefcnt aValue) : mValue(aValue) {} // only support prefix increment/decrement MOZ_ALWAYS_INLINE nsrefcnt operator++() { return ++mValue; } MOZ_ALWAYS_INLINE nsrefcnt operator--() { return --mValue; } MOZ_ALWAYS_INLINE nsrefcnt operator=(nsrefcnt aValue) { return (mValue = aValue); } MOZ_ALWAYS_INLINE operator nsrefcnt() const { return mValue; } MOZ_ALWAYS_INLINE nsrefcnt get() const { return mValue; } static const bool isThreadSafe = true;private: nsrefcnt operator++(int) = delete; nsrefcnt operator--(int) = delete; // In theory, RelaseAcquire consistency (but no weaker) is sufficient for // the counter. Making it weaker could speed up builds on ARM (but not x86), // but could break pre-existing code that assumes sequential consistency. Atomic<nsrefcnt> mValue;};
基本上這兩者的實作幾乎一模一樣,同樣都提供了 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,之後會介紹他的用途。
class nsAutoOwningThread
{
public:
nsAutoOwningThread() { mThread = PR_GetCurrentThread(); }
void* GetThread() const { return mThread; }
private:
void* mThread;
};
123456789
class nsAutoOwningThread{public: nsAutoOwningThread() { mThread = PR_GetCurrentThread(); } void GetThread() const { return mThread; } private: void mThread;};
接下來我們來看一下 AddRef 和 Release 兩者的實作,不管是否 thread safe ,實作方式都一樣透過
NS_IMPL_ADDREF(_class) 和
NS_IMPL_RELEASE(_class) 來達成,
#define NS_IMPL_ADDREF(_class) \
NS_IMETHODIMP_(MozExternalRefCountType) _class::AddRef(void) \
{ \
MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(_class) \
MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt"); \
if (!mRefCnt.isThreadSafe) \
NS_ASSERT_OWNINGTHREAD(_class); \
nsrefcnt count = ++mRefCnt; \
NS_LOG_ADDREF(this, count, #_class, sizeof(*this)); \
return count; \
}
1234567891011
#define NS_IMPL_ADDREF(_class) \NS_IMETHODIMP_(MozExternalRefCountType) _class::AddRef(void) \{ \ MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(_class) \ MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt"); \ if (!mRefCnt.isThreadSafe) \ NS_ASSERT_OWNINGTHREAD(_class); \ nsrefcnt count = ++mRefCnt; \ NS_LOG_ADDREF(this, count, #_class, sizeof(*this)); \ return count; \}
首先先看第一個 MACRO
MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING
#define MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(X) \
static_assert(!MOZ_IS_DESTRUCTIBLE(X) || \
mozilla::HasDangerousPublicDestructor::value, \
"略"); \
static_assert(!mozilla::HasDangerousPublicDestructor::value || \
MOZ_IS_DESTRUCTIBLE(X), \
"略");
1234567
#define MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(X) \ static_assert(!MOZ_IS_DESTRUCTIBLE(X) || \ mozilla::HasDangerousPublicDestructor::value, \ "略"); \ static_assert(!mozilla::HasDangerousPublicDestructor::value || \ MOZ_IS_DESTRUCTIBLE(X), \ "略");
這邊就是一開始提到的 XPCOM 物件不能有公開的解構子,利用 static_assert 在 compile time 做檢查,若
HasDangerousPublicDestructor::value 沒特別作手腳則預設是 false ,若真的有 public destructor 的需求,請針對 HasDangerousPublicDestructor 作特化 ,但這種需求再 Gecko code 裡面非常的少。要怎麼判斷一個 class 是否含有 public 解構子呢? Gecko 這邊有針對 compiler 版本作檢查,如果有支援,則直接使用 std::is_destructible 標準函式庫來作判斷,若不支援, Gecko 也有自己實作的版本
namespace mozilla {
struct IsDestructibleFallbackImpl
{
template<typename T> static T&& Declval();
template<typename T, typename = decltype(Declval<T>().~T())>
static TrueType Test(int);
template<typename>
static FalseType Test(...);
template<typename T>
struct Selector
{
typedef decltype(Test<T>(0)) type;
};
};
template<typename T>
struct IsDestructibleFallback
: IsDestructibleFallbackImpl::Selector<T>::type
{
};
}
#define MOZ_IS_DESTRUCTIBLE(X) (mozilla::IsDestructibleFallback<X>::value)
12345678910111213141516171819202122232425
namespace mozilla { struct IsDestructibleFallbackImpl { template<typename T> static T&& Declval(); template<typename T, typename = decltype(Declval<T>().~T())> static TrueType Test(int); template<typename> static FalseType Test(...); template<typename T> struct Selector { typedef decltype(Test<T>(0)) type; }; }; template<typename T> struct IsDestructibleFallback : IsDestructibleFallbackImpl::Selector<T>::type { }; }#define MOZ_IS_DESTRUCTIBLE(X) (mozilla::IsDestructibleFallback<X>::value)
這部份最關鍵的地方是用神妙的 template deduction 透過這行
template<typename T, typename = decltype(Declval<T>().~T())> 顯式呼叫解構子,在 compile time 推導出是否含有 public 解構子,達成確保 XPCOM 物件中沒有 expose public destructor 的條件限制。
回到剛剛 AddRef 的實作,
if (!mRefCnt.isThreadSafe) \
NS_ASSERT_OWNINGTHREAD(_class);
12
if (!mRefCnt.isThreadSafe) \NS_ASSERT_OWNINGTHREAD(_class);
假如 isThreadSafe 是 false 也就是說我們預期我們的 XPCOM 物件是執行在 single thread 上,會針對這個前提作一個 asssert 來確保這個假設是成立而沒有漏考慮的情況
NS_ASSERT_OWNINGTHREAD(_class) 展開後會是
if (MOZ_UNLIKELY(_class->_mOwningThread.GetThread() != PR_GetCurrentThread())) { \
MOZ_CRASH(msg); \
}
123
if (MOZ_UNLIKELY(_class->_mOwningThread.GetThread() != PR_GetCurrentThread())) { \ MOZ_CRASH(msg); \}
很明顯可以看出這邊判斷了一個我們一開始生成 XPCOM 物件所偷偷紀錄的 thread handle 是否跟目前所在的 thread handle 相同?如果不一樣則觸發 crash ,讓開發者好在 Debug build 或是 Nightly build 的時候能夠明確知道有與設計上衝突的情況發生,讓開發者能夠及早發現及早修正。反之,若是THREADSAFE版本的 XPCOM Object 則不需要執行這個判斷。接下來就是直接執行
nsrefcnt count = ++mRefCnt; 再將reference count return。
看完了 AddRef 後,Release 的實作也都大同小異
#define NS_IMPL_RELEASE_WITH_DESTROY(_class, _destroy) \
NS_IMETHODIMP_(MozExternalRefCountType) _class::Release(void) \
{ \
MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); \
if (!mRefCnt.isThreadSafe) \
NS_ASSERT_OWNINGTHREAD(_class); \
nsrefcnt count = --mRefCnt; \
NS_LOG_RELEASE(this, count, #_class); \
if (count == 0) { \
if (!mRefCnt.isThreadSafe) \
NS_ASSERT_OWNINGTHREAD(_class); \
mRefCnt = 1; / stabilize / \
_destroy; \
return 0; \
} \
return count; \
}
1234567891011121314151617
#define NS_IMPL_RELEASE_WITH_DESTROY(_class, _destroy) \NS_IMETHODIMP_(MozExternalRefCountType) _class::Release(void) \{ \ MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); \ if (!mRefCnt.isThreadSafe) \ NS_ASSERT_OWNINGTHREAD(_class); \ nsrefcnt count = --mRefCnt; \ NS_LOG_RELEASE(this, count, #_class); \ if (count == 0) { \ if (!mRefCnt.isThreadSafe) \ NS_ASSERT_OWNINGTHREAD(_class); \ mRefCnt = 1; / stabilize / \ _destroy; \ return 0; \ } \ return count; \}
當 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 ,實際的實作內容也都大同小異在這邊就不多作解釋了。