14

我正在玩TPL,並試圖找出我可以通過同時閱讀和寫入同一個詞典來得到多大的混亂。.Net中的Dictionary可能會在並行讀取和寫入時導致死鎖?

所以我有這樣的代碼:

private static void HowCouldARegularDicionaryDeadLock() 
    { 
     for (var i = 0; i < 20000; i++) 
     { 
      TryToReproduceProblem(); 
     } 
    } 

    private static void TryToReproduceProblem() 
    { 
     try 
     { 
      var dictionary = new Dictionary<int, int>(); 
      Enumerable.Range(0, 1000000) 
       .ToList() 
       .AsParallel() 
       .ForAll(n => 
       { 
        if (!dictionary.ContainsKey(n)) 
        { 
         dictionary[n] = n; //write 
        } 
        var readValue = dictionary[n]; //read 
       }); 
     } 
     catch (AggregateException e) 
     { 
      e.Flatten() 
       .InnerExceptions.ToList() 
       .ForEach(i => Console.WriteLine(i.Message)); 
     } 
    } 

這是非常搞砸確實,有很多拋出的異常的,主要是關於鍵不存在,一些有關索引越界陣列的。

但運行應用程序一段時間後,它掛起,並且CPU百分比保持在25%,機器有8個內核。 所以我認爲這是2個線程滿負荷運行。

enter image description here

然後我跑它dottrace,並得到了這一點:

enter image description here

據我的猜測一致,兩個線程運行在100%。

都運行Dictionary的FindEntry方法。

然後我再次運行應用程序,與dottrace,這一次的結果略有不同:

enter image description here

這個時候,一個線程在運行FindEntry,其他的插入。

我的第一個直覺是它已經死鎖了,但後來我認爲它不可能,只有一個共享資源,並沒有鎖定。

那麼應該如何解釋?

ps:我不想解決問題,它可以通過使用ConcurrentDictionary或通過並行聚合來解決。我只是在尋找一個合理的解釋。

+0

正如你所猜測的,Findentry試圖找到一個條目。它保留了一些稍後改變的局部變量,這導致循環結束條件永遠不會終止,因爲它假定由另一個線程改變的條目計數沒有改變。 –

+0

所以它不是一個死鎖,而是一個由內部狀態混亂造成的無限循環? – CuiPengFei

+0

是........... – pm100

回答

8

看起來像一個競爭條件(而不是死鎖) - 當你評論時,它會導致混亂的內部狀態。

該字典不是線程安全的,因此併發讀取和寫入到來自單獨線程的同一個容器(即使只有一個)也不安全。

一旦競爭條件被擊中,它變得不確定會發生什麼;在這種情況下,似乎是某種無限循環。

一般來說,一旦需要寫訪問權限,就需要某種形式的同步。

16

所以你的代碼正在執行Dictionary.FindEntry。它是而不是死鎖 - 當兩個線程阻塞時,會發生死鎖,使得它們互相等待以釋放資源,但在您的情況下,您會看到兩個看起來無限循環。線程未鎖定。

讓我們來看看這個方法在reference source

private int FindEntry(TKey key) { 
    if(key == null) { 
     ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); 
    } 

    if (buckets != null) { 
     int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; 
     for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) { 
      if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i; 
     } 
    } 
    return -1; 
} 

看看在for循環。 增量部分是i = entries[i].next,並猜測:entries是在Resize method中更新的字段。 next是內Entry struct的領域:

public int next;  // Index of next entry, -1 if last 

如果你的代碼不能退出FindEntry方法,最可能的原因是你已經成功地亂七八糟的條目以這樣的方式,他們生產的無限序列,當您跟蹤由next字段指向的索引時。

至於Insert method,它有一個非常類似的for循環:

​​

由於Dictionary類證明是非線程安全的,你在不確定的行爲境界是反正。

使用ConcurrentDictionary或鎖定模式諸如ReaderWriterLockSlimDictionary是線程安全併發只讀取)或普通老式lock很好地解決了這個問題。

+3

如果所有其他都不能讀取手冊。如果這也失敗了,請閱讀源代碼 - >終極手冊 – pm100

+0

@偉大的解釋夥計! (1) – Christos

相關問題