我明白在C++
爲防止多線程環境中的數據競爭,我們可以在類中添加一個mutex
。我們是否需要線程安全設計才能使用只讀方法的類?
但是,如果有一個簡單的類如下所示,它只有一個方法get()
,我們是否仍然需要考慮線程安全問題?
class SimpleClass {
public:
SimpleClass(int val) : v(val) {};
int get() { return v; }
private:
int v;
};
我明白在C++
爲防止多線程環境中的數據競爭,我們可以在類中添加一個mutex
。我們是否需要線程安全設計才能使用只讀方法的類?
但是,如果有一個簡單的類如下所示,它只有一個方法get()
,我們是否仍然需要考慮線程安全問題?
class SimpleClass {
public:
SimpleClass(int val) : v(val) {};
int get() { return v; }
private:
int v;
};
你的代碼是不安全的,你有一個潛在的競爭條件。
class SimpleClass {
public:
SimpleClass(int val) : v(val) {};
int get() { return v; }
private:
int v;
};
void thread_1(SimpleClass& sc)
{
std::cout << sc.get() << '\n';
}
void thread_2(SimpleClass& sc)
{
SimpleClass other(5);
sc = other; // potential race
}
的問題是編譯器生成一個賦值運算符允許您的類的對象爲,賦值爲它覆蓋內部數據。
這會導致潛在的種族。
感謝您指出這一點!那麼,只要我們禁用複製分配構造函數,代碼就會安全,如下所示? SimpleClass(SimpleClass const&)= delete; SimpleClass&operator =(SimpleClass const&)= delete; –
@ROBOTAI是的,儘管您只需要刪除*複製賦值運算符*而不是*複製構造函數*,因爲其他線程在構建過程中無法訪問它。 – Galik
如果這確實是整個類,而且也沒有辦法創建一個實例後更改的v
值,則類是不可變的,你不需要任何其他保護措施。不管線程調用get
,無論何時,仍會獲得實例初始化時的相同值。這裏沒有潛在的競爭條件。
什麼是賦值? 'SimpleClass a(1),b(2); a = b;' – Galik
要使代碼不安全,必須滿足四個條件。第三種情況只有在代碼包含寫入或更新時纔會發生。
在你的情況,考慮下面的[僞]代碼:
create new SimpleClass(1) in variable a
create new SimpleClass(2) in variable b
Switch a and b
{
create SimpleClass(a) into variable temp <-- with value 1
a=b <-- puts reference to b into variable a
b=temp <-- puts temp(value = 1) into variable b
}
如果這個代碼是由第二個線程在中間中斷,(B被分配到一後,但溫度之前爲分配給b),這將是不好的。
編輯。 (澄清@Juan在下面提出的觀點)。 因此,在你的情況下(這是SimpleClass
的情況,是的,因爲該類是不可變的,所以它本身是「線程安全的」,因爲它的代碼不會引起與類本身的競爭。但這並不意味着類不能在外部多線程代碼中使用這樣的方式來誘導的競爭條件。
在你的例子中確實存在競爭條件,但它不在類定義中,而是在使用它的代碼中。 SimpleClass仍然可以線程安全。因爲在完全構建之前不可能引用對象,因此內置的線程安全是通道的。顯然,單個實例不可能由多個線程同時構建。如果兩個線程同時創建同一個類的新實例,那麼他們實際上構造了兩個獨立的實例。唯一的問題可能是與內存模型相關的問題,正如我的答案中所解釋的。 – Juan
@Juan,是的,也許重要的一點是,在引用類或結構時使用術語「線程安全」有點誤導。它是代碼片段或代碼塊(可以位於類的內部,也可以使用類,其中包括不包含不安全代碼的類),這可能會導致不安全或導致競爭。 –
不可變類本質上是線程安全的。
粗略地說,併發問題發生在寫入可以與任何其他讀取或寫入相同數據的同時運行。
想想共享和排他鎖。通過獲取共享鎖來執行讀取,並且寫入需要獲得排他鎖。任何數量的線程都可以同時擁有一個共享鎖。只有一個線程可以同時擁有排他鎖,並且在排他鎖的情況下不能保持共享鎖。這意味着您可以同時執行讀取操作,但不能寫入,讀取和寫入。如果您的數據永遠不會被修改,那麼就不會有併發問題(不需要獨佔鎖,因此共享鎖沒有意義)。
這是函數式語言的優點之一:數據永遠不會被修改,使得函數本質上是線程安全的並且允許進行積極的編譯器優化。
現在,關於線程安全的另一個問題通常被遺忘:內存模型,特別是在現代NUMA體系結構中。
如果您瞭解volatile變量,那麼只要編程在單線程處理中保持正確,編譯器就可以自由優化數據訪問。
如果編譯器不知道另一個線程可能同時讀取或寫入一個變量,它可能會將該值保存在寄存器中,並且不會檢查主內存中的更改。這也可能發生在不同級別的緩存中的緩存值。它甚至可以優化條件分支,如果它知道編譯時的條件結果並且不知道所涉及的值可能不確定地改變。
聲明變量volatile表示它的值可能每次都改變並強制刷新到主內存並從主菜單讀取。
但是,如果這個值永遠不會改變,爲什麼會這樣呢?那麼,在施工期間價值會發生變化,這不能被認爲是瞬時的或原子的。如果編譯器不知道它是多線程的,甚至可能永遠不會將任何數據刷新到主內存。如果您引用此對象可用於其他線程,它將從主內存中讀取它,它從未初始化過。或者它甚至可以看到發生初始化(這可能發生在初始化Java舊版本中的大字符串時)。
我相信現代C++標準定義了一種內存模型,但我還沒有深入研究它。如果內存模型未指定或不夠強大,則可能需要執行基本操作,例如獲取或釋放鎖,這會建立「之前發生」關係。在任何情況下,你都必須告訴編譯器數據是不穩定的或不可變的,以便它可以提供正在使用的內存模型的保證。
在這種情況下,我會用const修飾符聲明變量和getter方法。我很肯定它可以正常工作,但我建議研究您正在使用的標準的內存模型,並根據需要切換到更新的標準。
只要你構造一個'SimpleClass',沒有其他方法可以改變'v'的副作用。 – CoryKramer
數據競爭需要*修改*訪問。如果你沒有這種機會,你就沒有可能參加比賽了。 –
@KerrekSB你的解釋解決了我的困惑。謝謝! –