2009-09-04 59 views
25

我有以下的代碼塊:C#中的yield return是否是線程安全的?

private Dictionary<object, object> items = new Dictionary<object, object>; 
public IEnumerable<object> Keys 
{ 
    get 
    { 
     foreach (object key in items.Keys) 
     { 
      yield return key; 
     } 
    } 
} 

這是線程安全的?如果沒有,我必須把lock圍繞循環或yield return

這裏是我的意思是:

線程1訪問Keys財產,而線程2增加了一個項目底層字典。 Thread1是否受到Thread2的影響?

+0

你的第二爲例將鎖定,返回一個枚舉然後解鎖。 你將在解鎖後迭代。 – Guillaume 2009-09-04 14:14:14

+0

哦,你是對的。我只注意到這是一個不好的例子。我將編輯該問題。 – Albic 2009-09-04 14:22:17

+0

只是爲了說明一點:通過鎖定線程安全性非常高,因此除非明確要求,否則自動鎖定每個動作都沒有意義。 – 2009-09-04 16:47:27

回答

7

好吧,我做了一些測試,並得到了一個有趣的結果。

看起來它比yield關鍵字更像是底層集合的枚舉器的問題。枚舉器(實際上它的MoveNext方法)拋出(如果正確實施)InvalidOperationException,因爲枚舉已更改。根據MSDN documentation of the MoveNext method這是預期的行爲。

因爲通過集合枚舉通常不是線程安全的,所以yield return也不是。

20

你說的線程安全究竟是什麼意思?

不管你是否在同一個線程中,你肯定不應該在迭代它時更改字典。

如果字典是在多個線程中被訪問在一般情況下,呼叫者應該取出的鎖(同一個覆蓋所有訪問),以便它們可以鎖定遍歷結果的持續時間。

編輯:爲了響應你的編輯,沒有它在沒有辦法對應的鎖碼。迭代器塊不會自動取出鎖定 - 它將如何知道syncRoot

此外,剛剛鎖定IEnumerable<TKey>的迴歸並不能使它線程安全或者 - 因爲鎖隻影響的時間段時,它的返回序列,而不是在此期間,它被遍歷的時期。

+0

是的,我的意思是字典而不是列表。第一次編輯是錯誤的(對不起),我刪除它。我在問題中加入了預期的行爲。 – Albic 2009-09-04 14:31:13

18

退房這個職位上與yield關鍵字幕後發生的事情:

Behind the scenes of the C# yield keyword

總之 - 編譯器把你的產量關鍵字,並在IL生成一個完整的類,以支持該功能。您可以在跳轉後查看頁面並查看生成的代碼......並且該代碼看起來像跟蹤線程ID以確保安全。

+0

+1爲真正有用的鏈接。 – Albic 2009-09-04 16:42:33

3

我相信這是,但我找不到一個證實它的參考。每當任何線程調用foreach所迭代器,一個新的線程局部*底層的IEnumerator的實例應該生成,所以不應該有任何「共享」內存狀態兩個線程可以在衝突...

  • 線程本地 - 在這個意義上說,它的參考變量的範圍是該線程上的方法棧幀
+0

是的,但如果調用GetEnumerator然後共享IEnumerator呢? 他應該澄清他在做什麼,他的線程。 – Guillaume 2009-09-04 14:19:22

3

我認爲收益實現是線程安全的。事實上,你可以在家裏運行這個簡單的程序,你會注意到listInt()方法的狀態被正確保存併爲每個線程恢復,沒有來自其他線程的邊緣效應。

public class Test 
{ 
    public void Display(int index) 
    { 
     foreach (int i in listInt()) 
     { 
      Console.WriteLine("Thread {0} says: {1}", index, i); 
      Thread.Sleep(1); 
     } 

    } 

    public IEnumerable<int> listInt() 
    { 
     for (int i = 0; i < 5; i++) 
     { 
      yield return i; 
     } 
    } 
} 

class MainApp 
{ 
    static void Main() 
    { 
     Test test = new Test(); 
     for (int i = 0; i < 4; i++) 
     { 
      int x = i; 
      Thread t = new Thread(p => { test.Display(x); }); 
      t.Start(); 
     } 

     // Wait for user 
     Console.ReadKey(); 
    } 
} 
+0

+1。我也只是驗證了由C#4.0編譯器生成的yield iterator狀態機是線程安全的。 – 2011-10-30 17:04:41

2
class Program 
{ 
    static SomeCollection _sc = new SomeCollection(); 

    static void Main(string[] args) 
    { 
     // Create one thread that adds entries and 
     // one thread that reads them 
     Thread t1 = new Thread(AddEntries); 
     Thread t2 = new Thread(EnumEntries); 

     t2.Start(_sc); 
     t1.Start(_sc); 
    } 

    static void AddEntries(object state) 
    { 
     SomeCollection sc = (SomeCollection)state; 

     for (int x = 0; x < 20; x++) 
     { 
      Trace.WriteLine("adding"); 
      sc.Add(x); 
      Trace.WriteLine("added"); 
      Thread.Sleep(x * 3); 
     } 
    } 

    static void EnumEntries(object state) 
    { 
     SomeCollection sc = (SomeCollection)state; 
     for (int x = 0; x < 10; x++) 
     { 
      Trace.WriteLine("Loop" + x); 
      foreach (int item in sc.AllValues) 
      { 
       Trace.Write(item + " "); 
      } 
      Thread.Sleep(30); 
      Trace.WriteLine(""); 
     } 
    } 
} 

class SomeCollection 
{ 
    private List<int> _collection = new List<int>(); 
    private object _sync = new object(); 

    public void Add(int i) 
    { 
     lock(_sync) 
     { 
      _collection.Add(i); 
     } 
    } 


    public IEnumerable<int> AllValues 
    { 
     get 
     { 
      lock (_sync) 
      { 
       foreach (int i in _collection) 
       { 
        yield return i; 
       } 
      } 
     } 
    } 
} 
+0

這是一個簡單的例子,顯示yield本身不是線程安全的。正如所寫,它是線程安全的,但如果您在AllValues中註釋掉鎖(_sync),則應該能夠通過運行幾次來驗證它是否不是線程安全的。如果你得到一個InvalidOperationException異常,它證明它不是線程安全的。 – sjp 2012-10-24 17:38:39