雙重檢查鎖定模式(DCLP)在無鎖編程方面是有點兒臭名昭著案例學術研究的味道。直到2004年,使用java開發並沒有安全的方式來實現它。在 c++11之前,使用便捷式c+開發並沒有安全的方式來實現它。由於引起人們關注的缺點模式暴露在這些語言之中,人們開始寫它。一組高調的java聚集在 一起開發人員並簽署了一項聲明,題為:“雙重檢查鎖定壞了”。在2004年斯科特 、梅爾斯和安德烈、亞歷山發表了一篇文章,題為:“c+與雙重檢查鎖定 的危險”對於DCLP是什麼?這兩篇文章都是偉大的引物,為什麼呢?在當時看來,這些語言都不足以實現它。
在過去。java現在可以為修訂內存模型,為thevolatileeyword注入新的語義,使得它盡可然安全實現DCLP.同樣地,c+11有一個全 新的內存模型和原子庫使得各種各樣的便捷式DCLP得以實現。c+11反過來啟發Mintomic,一個小型圖書館,我今年早些時候發布的,這使得它盡可 能的實現一些較舊的c/c++編譯器以及DCLP.
在這篇文章中,我將重點關注c++實現的DCLP.
什麼是雙重檢查鎖定?
假設你有一個類,它實現了著名的Singleton 模式,現在你想讓它變得線程安全。顯然的一個方法就是通過增加一個鎖來保證互斥共享。這樣的話,如果有兩個線程同時調用了Singleton::getInstance,將只有其中之一會創建這個單例。
- Singleton* Singleton::getInstance() { Lock lock; // scope-based lock, released automatically when the function returns if (m_instance == NULL) {
- m_instance = new Singleton;
- } return m_instance;
- }
這是完全合法的方法,但是一旦單例被創建,實際上就不再需要鎖了。鎖不一定慢,但是在高並發的條件下,不具有很好的伸縮性。
雙重檢查鎖定模式避免了在單例已經存在時候的鎖定。不過如Meyers-Alexandrescu的論文所顯示的,它並不簡單。在那篇論文中,作者描述了幾個有缺陷的用C++實現DCLP的嘗試,並剖析了每種情況為什麼是不安全的。最後,在第12頁,他們給出了一個安全的實現,但是它依賴於非指定的,特定平台的內存屏障(memory barriers)。
(譯注:內存屏障就是一種干預手段. 他們能保證處於內存屏障兩邊的內存操作滿足部分有序)
- Singleton* Singleton::getInstance() {
- Singleton* tmp = m_instance; ... // insert memory barrier if (tmp == NULL) {
- Lock lock;
- tmp = m_instance; if (tmp == NULL) {
- tmp = new Singleton; ... // insert memory barrier m_instance = tmp;
- }
- } return tmp;
- }
這裡,我們可以發現雙重檢查鎖定模式是由此得名的:在單例指針m_instance為NULL的時候,我們僅僅使用了一個鎖,這個鎖使偶然訪問到該單例的第一組線程繼續下去。而在鎖的內部,m_instance被再次檢查,這樣就只有第一個線程可以創建這個單例了。
這與可運行的實現非常相近。只是在突出顯示的幾行漏掉了某種內存屏障。在作者寫這篇論文的時候,還沒有填補此項空白的輕便的C/C++函數。現在,C++11已經有了。
用 C++11 獲得與釋放屏障
你可以用獲得與釋放屏障 安全的完成上述實現,在我以前的文章中我已經詳細的解釋過這個主題。不過,為了讓代碼真正的具有可移植性,你還必須要將m_instance包裝成原子類型,並且用放松的原子操作譯注:即非原子操作)來操作它。這裡給出的是結果代碼,獲取與釋放屏障部分高亮了。
- std::atomic<Singleton*> Singleton::m_instance;
- std::mutex Singleton::m_mutex;
- Singleton* Singleton::getInstance() {
- Singleton* tmp = m_instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire); if (tmp == nullptr) {
- std::lock_guard<std::mutex> lock(m_mutex);
- tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) {
- tmp = new Singleton; std::atomic_thread_fence(std::memory_order_release); m_instance.store(tmp, std::memory_order_relaxed);
- }
- } return tmp;
- }
即使是在多核系統上,它也可以令人信賴的工作,因為內存屏障在創建單例的線程與其後任何跳過這個鎖的線程之間,創建了一種同步的關系。Singleton::m_instance充當警衛變量,而單例本身的內容充當有效載荷。
所有那些有缺陷的DCLP實現都忽視了這一點:如果沒有同步的關系,將無法保證第一個線程的所有寫操作——特別是,那些在單例構造器中執行的寫操作——可以對第二個線程可見,雖然m_instance指針本身是可見的!第一個線程具有的鎖也對此無能為力,因為第二個線程不必獲得任何鎖,因此它能並發的運行。
如果你想更深入的理解這些屏障為什麼以及如何使得DCLP具有可信賴性,在我以前的文章中有一些背景信息,就像這個博客早前的文章一樣。
使用 Mintomic 屏障
Mintomic 是一個小型的C語言的庫,它提供了C++11原子庫的一個功能子集,其中包含有獲取與釋放屏障,而且它是運行於更老的編譯器之上的。Mintomic依賴於這樣的假設 ,即C++11的內存模型——特殊的是,其中包括無中生有的存儲 ——因為它不被更老的編譯器支持,不過這已經是我們不通過C++11能做到的最佳程度了。記住這些東西可是若干年來我們在寫多線程C++代碼時的環境。無 中生有的存儲Out-of-thin-air stores)已被時間證明是不流行的,而且好的編譯器也基本上不會這麼做。
這裡有一個DCLP的實現,就是用Mintomic來獲取與釋放屏障的。和前面使用C++11獲取和釋放屏障的例子比起來,它基本上是等效的。
- mint_atomicPtr_t Singleton::m_instance = { 0 };
- mint_mutex_t Singleton::m_mutex;
- Singleton* Singleton::getInstance() {
- Singleton* tmp = (Singleton*) mint_load_ptr_relaxed(&m_instance); mint_thread_fence_acquire(); if (tmp == NULL) {
- mint_mutex_lock(&m_mutex);
- tmp = (Singleton*) mint_load_ptr_relaxed(&m_instance); if (tmp == NULL) {
- tmp = new Singleton; mint_thread_fence_release(); mint_store_ptr_relaxed(&m_instance, tmp);
- }
- mint_mutex_unlock(&m_mutex);
- } return tmp;
- }