![](/img/trans.png)
[英]Does Interlocked guarantee visibility to other threads in C# or do I still have to use volatile?
[英]If I ensure two threads never run in parallel do I still have to make my list variable volatile?
想象一下,我有這樣的代碼,其中Windows窗體內部定時器我可以生成一些線程-但我保證只有一個線程使用下面的方法運行(從答案的一個指示在這里 -馬特·約翰遜):
nb:現在讓我們假設此_executing
方法有效,並且我不使用backgroundworker
等。
private volatile bool _executing;
private void TimerElapsed(object state)
{
if (_executing)
return;
_executing = true;
if(smth)
{
Thread myThread = new Thread(MainThread1);
myThread.IsBackground = true;
myThread.Start();
}else
{
Thread myThread = new Thread(MainThread2);
myThread.IsBackground = true;
myThread.Start();
}
}
public void MainThread1()
{
try
{
methodWhichAddelementTomyList(); // e.g., inside list.add();
}
finally
{_executing = false;}
}
public void MainThread2()
{
try
{
methodWhichAddelementTomyList(); // e.g., inside list.add();
}
finally
{_executing = false;}
}
現在,我還有List
實例變量,您可以從MainThread1
和MainThread2
訪問該實例變量,但是由於上述邏輯確保了MainThread1
和MainThread2
不會並行運行,因此我是否還必須使list
volatile
? 我可以遇到與緩存列表變量有關的問題嗎?
編輯 :並且這種方法也可以保護我免於並行運行那些線程嗎? (鏈接的問題中的答案有些不同-它在計時器內部運行工作-所以我想再次檢查)。
EDIT2 :老實說,下面是否應該在我的list
對象上應用volatile
關鍵字沒有共識。 這種狀況使我感到困惑。 因此,仍然歡迎記錄在案的答案; 否則無法完全解決
我將重申您的問題:
如果我確保兩個線程永遠不會並行運行,是否還必須使列表變量
volatile
?
您沒有兩個線程,只有三個線程:一個線程啟動另外兩個線程。 該線程始終與其他任何線程並行運行,並且使用共享標志與它們進行通信。 鑒於這一點,你發布的代碼,它不需要標記列表作為volatile
。
但是在只有兩個線程和兩個線程的情況下,它將以某種方式一個接一個地執行,而不會受到第三個線程的干擾(即,從共享變量中讀取),使列表volatile
就足以保證兩個線程始終可見相同的數據。
對於兩個不能同時運行以查看列表處於一致狀態(換句話說, 是最新狀態 )的線程,它們始終必須使用內存中最新版本的內存。 這意味着當線程開始使用列表時,必須在先前的寫操作結束后從列表中讀取。
這意味着內存障礙。 線程在使用列表之前需要獲取屏障,在使用列表之后需要釋放屏障。 使用Thread.MemoryBarrier
,您不能很好地控制屏障的語義,您總是會遇到完整的屏障(釋放和獲取,比我們需要的要堅固),但是最終結果是相同的。
因此,如果您可以保證線程永遠不會並行運行,那么C#內存模型可以保證以下各項能夠按預期工作:
private List<int> _list;
public void Process() {
try {
Thread.MemoryBarrier(); // Release + acquire. We only need the acquire.
_list.Add(42);
} finally {
Thread.MemoryBarrier(); // Release + acquire. We only need the release.
}
}
注意list是如何volatile
。 因為不需要,所以需要障礙。
現在的事情是, ECMA C#語言規范說(強調我的意思):
17.4.3揮發場
易失性字段的讀取稱為易失性讀取。 易失性讀取具有“ 獲取語義 ”; 就是說,它保證在指令序列中對內存的任何引用之前發生。
易失性字段的寫入稱為易失性寫入。 易失性寫入具有“ 釋放語義 ”; 即,保證在指令序列中的寫入指令之前的任何存儲器引用之后發生。
(感謝R. Martinho Fernandes在標准中找到了相關段落!)
換句話說,從volatile
字段讀取具有與獲取障礙相同的語義,而對volatile
字段進行寫入具有與釋放障礙相同的語義。 這意味着在給定前提的情況下,以下代碼節的行為與上一個相同: 1 :
private volatile List<int> _list;
public void Process() {
try {
// This is an acquire, because we're *reading* from a volatile field.
_list.Add(42);
} finally {
// This is a release, because we're *writing* to a volatile field.
_list = _list;
}
}
這就足以保證只要兩個線程不並行運行,它們將始終看到列表處於一致狀態。
(1) :這兩個示例並非嚴格相同,第一個示例提供了更強的保證,但是在這種特定情況下不需要這些強大的保證。
使對象對列表的引用具有可變性不會對列表本身產生任何影響。 它會影響您在讀取和分配該變量時所獲得的保證。
您不能在某個地方應用volatile並期望它神奇地使非線程安全的數據結構成為線程安全的。 如果真是那樣,那么穿線將很容易。 只需將所有內容標記為易失。 不起作用
從代碼和描述中可以看到,假設您僅在一個線程上訪問列表。 那不需要同步。 請注意,即使您在第二個線程上讀取了不安全的列表。 如果至少有一個寫程序,則不能有任何其他並發訪問。 甚至不讀。
這是一個更簡單的方法:
Task.Run(() => Process(smth));
...
public void Process(bool smth)
{
try
{
if (smth) methodWhichAddelementTomyList();
else otherThing();
}
finally
{_executing = false;}
}
沒有更多的“兩個線程”。 這是一個令人困惑的概念。
這里似乎有2個問題:
如果使用兩個線程,但它們從未異步運行,那么為什么要有兩個線程? 只要適當地序列化您的方法,即堅持一個線程。
但是,如果有兩個線程是某種要求(例如,允許一個線程繼續處理/保持暢通而另一個線程正在執行其他任務):即使您已對此進行編碼以確保沒有兩個線程可以同時訪問列表,為了安全起見,我將添加一個鎖定構造,因為列表不是線程安全的。 對我來說,這是最直接的。
您可以為此使用線程安全集合,例如System.Collections.Concurrent中的集合之一。 否則,您將需要同步對列表的所有訪問(即:將每個Add調用置於鎖中),
我個人避免使用volatile。 Albahari對此有一個很好的解釋 :“ volatile關鍵字可確保字段中始終存在最新的值。這是不正確的,因為正如我們所看到的,可以先進行寫操作再進行讀操作重新排序。”
揮發只是確保兩個線程同時看到相同的數據。 它根本不會阻止他們交錯讀取和寫入操作。
例如:聲明一個同步對象,例如:
private static Object _objectLock = new Object();
並在methodWhichAddelementTomyList
方法(以及列表的其他任何地方被修改)中使用,以確保從不同線程對資源的串行訪問:
lock(_objectLock)
{
list.Add(object);
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.