2009-09-01 37 views
20

好吧,我無法正確理解多線程場景。對不起再次提出類似的問題,我只是在互聯網上看到許多不同的「事實」。在多線程場景中正確鎖定列表<T>?

public static class MyClass { 
    private static List<string> _myList = new List<string>; 
    private static bool _record; 

    public static void StartRecording() 
    { 
     _myList.Clear(); 
     _record = true; 
    } 

    public static IEnumerable<string> StopRecording() 
    { 
     _record = false; 
     // Return a Read-Only copy of the list data 
     var result = new List<string>(_myList).AsReadOnly(); 
     _myList.Clear(); 
     return result; 
    } 

    public static void DoSomething() 
    { 
     if(_record) _myList.Add("Test"); 
     // More, but unrelated actions 
    } 
} 

的想法是,如果錄像被激活,調用DoSomething的()獲取記錄在內部列表和StopRecording()被調用時返回。

我的規格是這樣的:

  • 的StartRecording不被認爲是線程安全的。用戶應該在沒有其他線程調用DoSomething()的情況下調用此方法。但是,如果它有可能,那會很好。
  • StopRecording也不是正式的線程安全的。再說一次,如果可能的話,它會很好,但那不是必需的。
  • DoSomething的必須是線程安全的

的常用方法似乎是:

public static void DoSomething() 
    { 
     object _lock = new object(); 
     lock(_lock){ 
      if(_record) _myList.Add("Test"); 
     } 
     // More, but unrelated actions 
    } 

另外,聲明一個靜態變量:

private static object _lock; 

    public static void DoSomething() 
    { 
     lock(_lock){ 
      if(_record) _myList.Add("Test"); 
     } 
     // More, but unrelated actions 
    } 

然而,this answer說,這不會阻止其他代碼訪問它。

所以我想

  • 我將如何正確地鎖定一個列表?
  • 我應該在我的函數中創建鎖對象還是作爲靜態類變量?
  • 我可以將Start和StopRecording的功能封裝在鎖定塊中嗎?
  • StopRecording()會做兩件事:將布爾變量設置爲false(以防止DoSomething()添加更多東西),然後複製列表以將數據的副本返回給調用者。我假設_record = false;是原子的,將立即生效?所以通常我根本不用擔心多線程,除非其他線程再次調用StartRecording()。

在一天結束時,我正在尋找一種方式來表達「好的,這個列表是我的,現在所有其他線程都必須等到我完成它」。

+0

如果我明白你需要看看,我有同樣的問題 https://stackoverflow.com/a/44414120/4745606 – 2017-06-07 13:33:32

回答

27

我將鎖定_myList本身,因爲它是私有的,但使用單獨的變量更常見。爲了改善幾點:

public static class MyClass 
{ 
    private static List<string> _myList = new List<string>; 
    private static bool _record; 

    public static void StartRecording() 
    { 
     lock(_myList) // lock on the list 
     { 
      _myList.Clear(); 
      _record = true; 
     } 
    } 

    public static IEnumerable<string> StopRecording() 
    { 
     lock(_myList) 
     { 
      _record = false; 
      // Return a Read-Only copy of the list data 
      var result = new List<string>(_myList).AsReadOnly(); 
      _myList.Clear(); 
      return result; 
     } 
    } 

    public static void DoSomething() 
    { 
     lock(_myList) 
     { 
      if(_record) _myList.Add("Test"); 
     } 
     // More, but unrelated actions 
    } 
} 

請注意,此代碼使用lock(_myList)同步訪問都_myList _Record。而且你需要同步這兩者的所有操作。

爲了與此處的其他答案保持一致,lock(_myList)對_myList沒有任何作用,它只是使用_myList作爲標記(可能是作爲HashSet中的鍵)。所有方法都必須使用相同的標記詢問權限才能公平。另一個線程上的方法仍然可以使用_myList而不首先鎖定,但會產生不可預知的結果。

我們可以使用任何標記,所以我們通常會創建一個專門:

private static object _listLock = new object(); 

然後用lock(_listLock),而不是lock(_myList)無處不在。

如果myList是公開的,這種技術將是明智的,如果您重新創建了myList而不是調用Clear(),這將是絕對必要的。

+1

+1提及直接鎖定列表的危險。 – 2009-09-01 17:20:13

+2

這裏的所有答案都非常有幫助(所以請親愛的用戶誰來自谷歌搜索,閱讀全部),但我接受這一點,因爲「令牌」,其中立即使我的概念清晰。我仍然創建了自己的私有隻讀靜態對象RecordingLock,僅僅是因爲我想爲它命名。 – 2009-09-01 23:24:00

12

創建一個新的鎖定在DoSomething()肯定是錯的 - 這將是毫無意義的,因爲每次調用DoSomething()會使用不同的鎖。你應該使用第二種形式,但有一個初始化:

private static object _lock = new object(); 

這是真的,鎖定沒有訪問列表阻止別的什麼,但除非你是直接暴露在列表中,這並不重要:無論如何,沒有其他人會訪問這個列表。

是的,你可以用同樣的方式將開始/停止錄製包裝在鎖中。

是的,設置一個布爾變量是原子的,但是這並不能保證它是線程安全的。如果您只能訪問同一鎖定內的變量,您儘管在原子性波動性方面沒有問題。否則,您可能會看到「陳舊」值 - 例如您在一個線程中將值設置爲true,另一個線程在讀取時可以使用緩存的值。

+0

因此,如果DoSomething()正在使用一個鎖和三個線程訪問函數,那麼另外兩個線程將等到第一個完成?如果開始/停止使用相同的鎖,該怎麼辦?所有請求都會排隊嗎?或者可能發生錯誤的事情? – 2009-09-01 15:25:01

+0

所有的請求將排隊。基本上,'lock'語句是'Monitor.Enter'的糖,它會阻塞,直到獲得鎖。 – 2009-09-01 15:28:33

+2

是的,所有的方法都應該使用__same object__來鎖定。它是一個用於鎖定的替身(_myList) – 2009-09-01 15:35:08

1

您可能會誤解this answer,實際上聲明的是他們lock語句實際上並未鎖定正在修改的對象,而是阻止任何其他代碼使用該對象作爲鎖定源執行。

這實際上意味着什麼,當你使用相同的實例作爲鎖定對象時,鎖定塊內的代碼不應該被執行。

實質上,你並沒有真的試圖「鎖定」你的列表,你試圖擁有一個可用作參考點的公共實例,以便當你想修改你的列表,當它正在使用或「鎖定「,你想防止其他代碼執行,可能會修改列表。

+0

好的,所以設計和一般來說:如果我有一個或多個需要線程安全修改的變量,我應該有一個通用的鎖對象,因爲每個鎖對象都可以只能被一個線程鎖定。但是我可能有lockObject1和lockObject2,它們都可以修改我的列表並導致所有通常的多線程問題。 – 2009-09-01 15:51:21

+1

是的,因爲單獨的線程可以鎖定不同的對象實例,並且因爲它們每個都對所討論的特定對象具有鎖定,所以鎖塊內的代碼將執行。就像喬恩曾經提到的那樣,它本質上是一個Monitor.Enter調用,該調用會阻塞,直到鎖定的對象實例已經在上下文中執行了與其關聯的代碼。 – 2009-09-01 16:26:08

3

第一種方法是錯誤,因爲每個調用者都會鎖定不同的對象。 您可以鎖定列表。

lock(_myList) 
{ 
    _myList.Add(...) 
} 
6

有幾種方法來鎖定列表。您可以直接鎖定_myList 提供_myList從未更改爲引用新列表。

lock (_myList) 
{ 
    // do something with the list... 
} 

您可以爲此專門創建一個鎖定的對象。

private static object _syncLock = new object(); 
lock (_syncLock) 
{ 
    // do something with the list... 
} 

如果靜態集合實現了System.Collections.ICollection接口(名單(T)所做的),您還可以同步使用SyncRoot屬性。

lock (((ICollection)_myList).SyncRoot) 
{ 
    // do something with the list... 
} 

主要的一點明白的是,你要一個只有一個對象爲您鎖定前哨,這就是爲什麼創建DoSomething的()函數韓元內鎖定前哨使用沒有工作。正如Jon所說,每個調用DoSomething()的線程都會獲得它自己的對象,所以每次對該對象的鎖定都會成功並授予對該列表的即時訪問權限。通過使鎖定對象成爲靜態(通過列表本身,專用鎖定對象或ICollection.SyncRoot屬性),它將在所有線程間共享,並且可以有效地序列化對列表的訪問。