簡體   English   中英

如果我確保兩個線程永遠不會並行運行,是否還必須使列表變量可變?

[英]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實例變量,您可以從MainThread1MainThread2訪問該實例變量,但是由於上述邏輯確保了MainThread1MainThread2不會並行運行,因此我是否還必須使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個問題:

  1. 如果使用兩個線程,但它們從未異步運行,那么為什么要有兩個線程? 只要適當地序列化您的方法,即堅持一個線程。

  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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM