2010-03-22 20 views
24

我知道,已經取得了一對夫婦面前的問題/答案很清楚,那volatile是關係到C的可見狀態++內存模型,而不是多線程。可能揮發可以在用戶定義的類型,以幫助編寫線程安全的代碼

另一方面,Alexandrescu的article使用volatile關鍵字不是作爲運行時功能,而是作爲編譯時檢查強制編譯器無法接受可能不是線程安全的代碼。在文章中,關鍵字的使用更像是required_thread_safety標籤,而不是實際使用的volatile

這是(AB)利用volatile合適?該方法可能隱藏了哪些可能的陷阱?

首先想到的是添加混淆:volatile與線程安全無關,但由於缺乏更好的工具我可以接受它。

文章的基本簡化:

如果聲明一個變量volatile,只有volatile成員方法可以調用它,所以編譯器將阻止調用代碼的其他方法。聲明std::vector實例爲volatile將阻止該類的所有用途。添加一個形狀爲鎖定指針的包裝器,該指針執行const_cast以釋放volatile要求,任何通過鎖定指針的訪問都將被允許。

從文章偷竊:

template <typename T> 
class LockingPtr { 
public: 
    // Constructors/destructors 
    LockingPtr(volatile T& obj, Mutex& mtx) 
     : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) 
    { mtx.Lock(); } 
    ~LockingPtr() { pMtx_->Unlock(); } 
    // Pointer behavior 
    T& operator*() { return *pObj_; } 
    T* operator->() { return pObj_; } 
private: 
    T* pObj_; 
    Mutex* pMtx_; 
    LockingPtr(const LockingPtr&); 
    LockingPtr& operator=(const LockingPtr&); 
}; 

class SyncBuf { 
public: 
    void Thread1() { 
     LockingPtr<BufT> lpBuf(buffer_, mtx_); 
     BufT::iterator i = lpBuf->begin(); 
     for (; i != lpBuf->end(); ++i) { 
     // ... use *i ... 
     } 
    } 
    void Thread2(); 
private: 
    typedef vector<char> BufT; 
    volatile BufT buffer_; 
    Mutex mtx_; // controls access to buffer_ 
}; 

注意

後的第一對夫婦的答案的出現,我想我必須澄清,我可能還沒有使用最合適的詞。

使用的volatile是因爲,因爲它意味着在編譯時它所提供的在運行但不是。也就是說,同樣的招數可以與const關鍵字被拉如果它是在用戶定義類型,很少使用是volatile是。也就是說,有一個關鍵字(它恰好被拼寫爲volatile)允許我阻止成員函數調用,Alexandrescu正在使用它來欺騙編譯器無法編譯線程不安全的代碼。

我認爲這是很多元編程技巧,在那裏,不是因爲他們在編譯的時候做了什麼,而是它所強制編譯器爲你做。

+2

他們正在討論目前在comp.lang.C++。moderated中的代碼。 – 2010-03-22 20:12:49

+2

@Johannes:這是他們每十年必須進行兩次的討論嗎?我記得Andrei發表這篇文章時激烈的討論。就像這裏一樣,大部分的熱量是由於人們只是在同一篇文章中閱讀「易失性」和「線索」而引起的,並且甚至沒有試圖理解這個想法是什麼。 – sbi 2011-04-05 07:29:36

回答

0

你必須更好地這樣做。 不穩定甚至沒有發明提供線程安全。它是爲了正確訪問內存映射的硬件寄存器而發明的。 易失性關鍵字對CPU無序執行功能沒有影響。您應該使用正確的OS調用或CPU定義的CAS指令,內存隔離等。

CAS

Memory Fence

+0

通過查看所提供的代碼,我看不到使BufT變得易變的收益。整個線程安全的事情現在是Mutex的問題。由於BufT是私有成員,因此性能方面最好不要變化。但是我也看到你的代碼存在問題。一旦Thread1鎖定Mutex,Thread2將永遠無法訪問BufT,因此它將停留在其主體的第一行。 – Malkocoglu 2010-03-22 11:30:53

+2

如果您仔細閱讀文章和代碼,您會發現變量在使用時不是'volatile'(該限定符是通過'const_cast'移除的)。這只是一個編譯時間的伎倆。正如@Malkocoglu指出的那樣,整個線程的安全性通過互斥體正確處理。我猜混淆是該方法的一大缺陷 – 2010-03-22 12:21:42

+0

-1:我猶豫了下來,但最終我做到了,因爲在文章或這篇文章中沒有使用volatile來確保無序執行以certian方式進行。它用於利用編譯器的類型系統,所以你的帖子與問題無關。 – 2010-03-22 14:40:55

-2

我就不具體Alexandrescu的的意見,是否是合理的認識,但是,對於所有的,我尊重他作爲一個超級聰明的傢伙,他的治療揮發性的語義表明,他踩他的專業領域之外。多線程中揮發性是絕對沒有價值的(請參閱here以獲得對該主題的良好處理),所以Alexandrescu聲稱易失性對多線程訪問很有用,這讓我很想知道我可以在他的文章的其餘部分放置多少信心。

+2

我認爲你誤解了這篇文章,使用'volatile'關鍵字的唯一原因是因爲它在編譯時意味着什麼,而不是在運行時。在運行時,所有使用(即編譯)在對實例進行操作之前刪除volatile變量:編譯後的代碼中沒有volatile,因爲所有用途都經過LockPtr內的const_cast指針,易失性 – 2010-03-22 12:19:15

+1

從您鏈接到的文章,「漢斯Boehm指出,只有三個便攜式用於易失性」。 Hans Boehm是錯誤的(雖然不是他應該感到羞愧的) - Alexandrescu提出了第四種使用volatile的方法,即依靠其常規式的傳染性行爲,而不是其與存儲器訪問相關的語義。 – 2010-03-22 13:13:04

+0

@David:這篇文章沒有給出任何解釋的餘地​​:「它的目的是與不同線程中訪問和修改的變量結合使用。基本上,沒有易變性,編寫多線程程序變得不可能,或者編譯器浪費大量優化機會「。 我明白這篇文章的主要內容是使用'volatile'作爲一種正交const限定詞。我從不否認這一點。我只是對那些明顯不明白其原意的人寫了一篇關於「易變」的文章表示保留。 – 2010-03-22 13:21:04

6

我認爲這個問題不是由volatile提供的線程安全問題。它並沒有和安德烈的文章不說。在這裏,使用mutex來實現這一點。問題是,是否使用volatile關鍵字提供靜態類型檢查隨着互斥體使用線程安全代碼,是否濫用了volatile關鍵字?恕我直言,這是非常聰明的,但我遇到過的開發者不是嚴格類型檢查只是爲了它。

IMO在爲多線程環境編寫代碼時,已經有足夠的謹慎來強調,在這種情況下,您會期望人們不會無視種族條件和死鎖。

此包裝方法的缺點是,使用LockingPtr包裝的類型上的每個操作都必須通過成員函數。這會增加一個層次的間接性,這可能會大大影響開發團隊的舒適度。

但如果你是誰,在C++的精神,相信a.k.a 一個純粹嚴格型檢查;這是一個很好的選擇。

+1

+1。我不同意額外程度的間接不適。任何進行多線程的人都必須知道,鎖定互斥鎖是在那裏的代價高昂的操作。與鎖本身相比,額外的間接水平幾乎沒有任何意義。 – 2010-03-22 12:15:25

+1

@dribeas:我的意思是語法不適。相信我,一些開發人員不喜歡編寫包裝代碼,而不是使用簡單直接的代碼。 – Abhay 2010-03-22 12:19:01

4

這會捕獲某些類型的線程不安全的代碼(併發訪問),但會錯過其他類型(由於鎖定反轉造成的死鎖)。既不是特別容易測試,所以這是一個溫和的部分勝利。在實踐中,記住強制一個特定私有成員只能在某個指定的鎖下訪問的約束對我來說不是一個大問題。

這個問題的兩個答案已經證明你認爲混淆是一個顯着的缺點是正確的 - 維護者可能已經非常有條件理解volatile的內存訪問語義與線程安全無關,在聲明它不正確之前,他們甚至不會閱讀代碼/文章的其餘部分。

我認爲Alexandriacu在文章中概述的另一個大缺點是它不適用於非類類型。這可能是一個難以記住的限制。如果您認爲標記數據成員volatile會停止在不鎖定的情況下使用它們,然後希望編譯器告訴您什麼時候鎖定,那麼您可能會意外地將它應用於int或模板參數相關類型的成員。由此產生的不正確的代碼將編譯好,,但您可能已經停止檢查您的代碼是否存在此類錯誤。想象一下,如果可以分配給const int,那麼會發生錯誤,尤其是在模板代碼中,但程序員仍希望編譯器檢查它們的const正確性。

我認爲數據成員的類型實際上具有任何volatile成員函數的風險應該被記錄下來,然後打折,儘管它可能會在某天某個時候咬人。

我不知道是否有什麼要說的編譯器通過屬性提供額外的常規風格類型修飾符。 Stroustrup says,「建議使用屬性來控制不影響程序含義但可能有助於檢測錯誤的事物」。如果你可以用代碼[[__typemodifier(needslocking)]]代替volatile的所有提及,那麼我認爲它會更好。如果沒有const_cast,就不可能使用該對象,並且希望在不考慮丟棄的內容的情況下不會寫出const_cast

+0

在這個答案中有幾個有趣的珍珠:取決於部分解決方案可能會讓你忘記那些無助的情況。另一個好的方面是可能使用屬性(即使我沒有看到如何真正發揮屬性的訣竅,它需要某種類型的'attribute_cast')。我曾考慮過具有volatile成員函數但沒有看到任何實際代碼的類,我想知道這個問題到底有多深。謝謝 – 2010-03-22 14:17:45

+3

+1。他抨擊C++類型系統給「volatile」賦予新的含義。作爲一個方面說明:在'LockingPtr(volatile T&obj,Mutex&mtx) :pObj_(const_cast (&obj)), pMtx _(&mtx)'是未定義的行爲,根據6.7.3 5)的標準,不是嗎? – stephan 2010-03-22 14:25:22

+1

@dribeas:好吧,在我剛剛發明的'[[__typemodifier(X)]]''的定義中,'const_cast'可以移除任何類型修飾符,包括用戶創建的類型修飾符,就像它當前移除const '和'volatile'。由於用戶定義的類型修飾符除了防止不兼容的賦值之外,在語言中沒有任何意義,所以這是「安全的」,前提是不要以違反修飾符在此程序中隱含的不變量的方式使用結果。我沒有對這個想法進行徹底的同行評審,或者實際上已經考慮了這個想法的時間比它花費的時間更長;-) – 2010-03-22 14:43:47

1

從不同的角度來看待這個。當你聲明一個變量爲const時,你告訴編譯器該值不能被你的代碼改變。但這並不意味着價值將不會更改。例如,如果你這樣做:

const int cv = 123; 
int* that = const_cast<int*>(&cv); 
*that = 42; 

...這根據標準喚起不確定的行爲,但在實踐中會發生點什麼。也許價值會改變。也許會有一個sigfault。也許飛行模擬器將啓動 - 誰知道。關鍵是你不知道在平臺無關的基礎上會發生什麼。因此const的承諾不履行。該值可能實際上可能不是const。

現在,鑑於這是真的,使用const濫用該語言?當然不是。它仍然是該語言提供的一種工具,可以幫助您編寫更好的代碼。它永遠不會是最終所有的工具,以確保價值保持不變 - 程序員的大腦是最終的工具 - 但這是否使const無用?

我說不,使用const作爲工具來幫助你編寫更好的代碼不是對語言的濫用。事實上,我會更進一步,並說這是該功能的意圖

現在,揮發性也是如此。將volatile聲明爲不會使程序線程安全。它可能甚至不會使該變量或對象線程安全。但是編譯器會執行CV認證語義,細心的程序員可以利用這個事實來幫助他編寫更好的代碼,幫助編譯器確定他可能編寫錯誤的地方。就像編譯器可以幫助他時,他試圖做到這一點:

const int cv = 123; 
cv = 42; // ERROR - compiler complains that the programmer is potentially making a mistake 

忘記內存柵欄和volatile對象和變量的原子性,就像你早已忘記cv的真實常量性。但使用該語言爲您編寫更好的代碼的工具。其中一種工具是volatile

2

C++ 03§7.1.5.1p7:

如果試圖指通過使用左值與非易失性限定與揮發性限定類型定義的對象類型,程序行爲是未定義的。

因爲您示例中的buffer_被定義爲volatile,所以將其轉換爲未定義的行爲。然而,讓您可以與定義的對象作爲非易失性的適配器,但增加了波動性:

template<class T> 
struct Lock; 

template<class T, class Mutex> 
struct Volatile { 
    Volatile() : _data() {} 
    Volatile(T const &data) : _data (data) {} 

    T  volatile& operator*()  { return _data; } 
    T const volatile& operator*() const { return _data; } 

    T  volatile* operator->()  { return &**this; } 
    T const volatile* operator->() const { return &**this; } 

private: 
    T _data; 
    Mutex _mutex; 

    friend class Lock<T>; 
}; 

需要的友誼,嚴格控制通過已鎖定的對象非易失性訪問:

template<class T> 
struct Lock { 
    Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); } 
    ~Lock() { _data._mutex.unlock(); } 

    T& operator*() { return _data._data; } 
    T* operator->() { return &**this; } 

private: 
    Volatile<T> &_data; 
}; 

實施例:

struct Something { 
    void action() volatile; // Does action in a thread-safe way. 
    void action(); // May assume only one thread has access to the object. 
    int n; 
}; 
Volatile<Something> data; 
void example() { 
    data->action(); // Calls volatile action. 
    Lock<Something> locked (data); 
    locked->action(); // Calls non-volatile action. 
} 

有兩個警告。首先,你仍然可以訪問公共數據成員(Something :: n),但他們將是合格的易變的;這可能會在各個方面失敗。第二,東西不知道,如果它已被定義,如果真的已被定義爲揮發性和鑄造走揮發性的方法(從「本」或成員)仍然是UB這樣:

Something volatile v; 
v.action(); // Compiles, but is UB if action casts away volatile internally. 

主要目標得以實現:對象不必意識到它們以這種方式使用,並且編譯器將阻止對非易失性方法(這是大多數類型的所有方法)的調用,除非您明確地通過鎖定。

2

Building on other code並且完全不需要揮發性指定符,這不僅適用,而且可以正確地傳播const(類似於iterator vs const_iterator)。不幸的是,它需要相當多的兩種接口類型的樣板代碼,但是您不必重複任何方法的邏輯:即使您必須類似地「複製」「易失性」版本,每個方法仍然定義一次在常量和非常量上正常重載方法。

#include <cassert> 
#include <iostream> 

struct ExampleMutex { // Purely for the sake of this example. 
    ExampleMutex() : _locked (false) {} 
    bool try_lock() { 
    if (_locked) return false; 
    _locked = true; 
    return true; 
    } 
    void lock() { 
    bool acquired = try_lock(); 
    assert(acquired); 
    } 
    void unlock() { 
    assert(_locked); 
    _locked = false; 
    } 
private: 
    bool _locked; 
}; 

// Customization point so these don't have to be implemented as nested types: 
template<class T> 
struct VolatileTraits { 
    typedef typename T::VolatileInterface  Interface; 
    typedef typename T::VolatileConstInterface ConstInterface; 
}; 

template<class T> 
class Lock; 
template<class T> 
class ConstLock; 

template<class T, class Mutex=ExampleMutex> 
struct Volatile { 
    typedef typename VolatileTraits<T>::Interface  Interface; 
    typedef typename VolatileTraits<T>::ConstInterface ConstInterface; 

    Volatile() : _data() {} 
    Volatile(T const &data) : _data (data) {} 

    Interface  operator*()  { return _data; } 
    ConstInterface operator*() const { return _data; } 
    Interface  operator->()  { return _data; } 
    ConstInterface operator->() const { return _data; } 

private: 
    T _data; 
    mutable Mutex _mutex; 

    friend class Lock<T>; 
    friend class ConstLock<T>; 
}; 

template<class T> 
struct Lock { 
    Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); } 
    ~Lock() { _data._mutex.unlock(); } 

    T& operator*() { return _data._data; } 
    T* operator->() { return &**this; } 

private: 
    Volatile<T> &_data; 
}; 

template<class T> 
struct ConstLock { 
    ConstLock(Volatile<T> const &data) : _data (data) { _data._mutex.lock(); } 
    ~ConstLock() { _data._mutex.unlock(); } 

    T const& operator*() { return _data._data; } 
    T const* operator->() { return &**this; } 

private: 
    Volatile<T> const &_data; 
}; 

struct Something { 
    class VolatileConstInterface; 
    struct VolatileInterface { 
    // A bit of boilerplate: 
    VolatileInterface(Something &x) : base (&x) {} 
    VolatileInterface const* operator->() const { return this; } 

    void action() const { 
     base->_do("in a thread-safe way"); 
    } 

    private: 
    Something *base; 

    friend class VolatileConstInterface; 
    }; 

    struct VolatileConstInterface { 
    // A bit of boilerplate: 
    VolatileConstInterface(Something const &x) : base (&x) {} 
    VolatileConstInterface(VolatileInterface x) : base (x.base) {} 
    VolatileConstInterface const* operator->() const { return this; } 

    void action() const { 
     base->_do("in a thread-safe way to a const object"); 
    } 

    private: 
    Something const *base; 
    }; 

    void action() { 
    _do("knowing only one thread accesses this object"); 
    } 

    void action() const { 
    _do("knowing only one thread accesses this const object"); 
    } 

private: 
    void _do(char const *restriction) const { 
    std::cout << "do action " << restriction << '\n'; 
    } 
}; 

int main() { 
    Volatile<Something> x; 
    Volatile<Something> const c; 

    x->action(); 
    c->action(); 

    { 
    Lock<Something> locked (x); 
    locked->action(); 
    } 

    { 
    ConstLock<Something> locked (x); // ConstLock from non-const object 
    locked->action(); 
    } 

    { 
    ConstLock<Something> locked (c); 
    locked->action(); 
    } 

    return 0; 
} 

比較類的東西到什麼Alexandrescu的的使用volatile將需要:

struct Something { 
    void action() volatile { 
    _do("in a thread-safe way"); 
    } 

    void action() const volatile { 
    _do("in a thread-safe way to a const object"); 
    } 

    void action() { 
    _do("knowing only one thread accesses this object"); 
    } 

    void action() const { 
    _do("knowing only one thread accesses this const object"); 
    } 

private: 
    void _do(char const *restriction) const volatile { 
    std::cout << "do action " << restriction << '\n'; 
    } 
}; 
+0

Roger?你呢? – sbi 2011-02-02 18:23:23

0

在文章的關鍵字用來更像比實際用途的揮發性一個required_thread_safety標籤。

沒有閱讀過文章 - 爲什麼Andrei沒有使用required_thread_safety標籤呢?濫用volatile在這裏聽起來不太好。我相信這導致更多混亂(就像你說的),而不是避免它。

也就是說,volatile有時在多線程代碼需要,即使它不是一個充分條件,只是爲了防止編譯器優化掉依賴於一個值的異步更新檢查。

+0

使用'volatile'而不是標籤的原因是類型系統可以在編譯時使用它來標記無效訪問。現在你提到這一點,我將不得不考慮基於標籤的東西是否可以工作... – 2011-02-02 19:50:58

+0

@David:我認爲它應該可以工作,但我很樂意承認它可能更難實現。首先,你需要實現一個智能指針界面。也許。有沒有真的想過這件事。 – 2011-02-02 20:09:02

相關問題