【问题标题】:C# Dictionary iteration - unexpected resultsC# 字典迭代 - 意外结果
【发布时间】:2013-10-15 14:13:10
【问题描述】:

做了一个小例子来选择多线程环境中频繁更新字典的最佳方案。然后观察到“奇怪”的迭代行为。

using System;
using System.Collections.Generic;
using System.Threading;

namespace DictionaryTest
{
    class Program
    {
        static int _n;
        static Dictionary<int, string> _dict = new Dictionary<int, string>();

        static void Main(string[] args) {
            for (int i = 0; i < 50; i++) _dict[i] = "FIRST";

            new Thread(ReadDict).Start();
            Thread.Sleep(30);

            // CASE A, Throws exception AS EXPECTED.
            //_dict.Clear();

            // CASE B, ReadDict continues iterate on old dictionary values!!!???
            //_dict = new Dictionary<int, string>();

            // CASE C
            UpdateDict();
            ReadDict();

            Console.ReadKey();
        }

        private static void ReadDict() {
            // Read Method X
            // (continues iterate on old dictionary values!!!???)
            //
            foreach (var kvp in _dict) {
                Thread.Sleep(3);
                Console.WriteLine("{0,3} {1,4} {2,6} :{3,5} Method X", Interlocked.Increment(ref _n), kvp.Key, kvp.Value, Thread.CurrentThread.ManagedThreadId);
            }

            // Read Method Y
            //
            //for (int i = 0; i < 50; i++) {
            //    Thread.Sleep(3);
            //    Console.WriteLine("{0,3} {1,4} {2,6} :{3,5} Method Y", Interlocked.Increment(ref _n), i, _dict[i], Thread.CurrentThread.ManagedThreadId);
            //}
        }

        private static void UpdateDict() {
            var tmp = new Dictionary<int, string>();
            for (int i = 0; i < 50; i++) tmp[i] = "SECOND";
            _dict = new Dictionary<int, string>(tmp);
        }
    }
}

组合:

  1. CASE A 和(方法 X 或方法 Y) - 按预期抛出异常!
  2. 案例 B 和方法 X - 继续迭代旧字典值???
  3. 案例 C 和方法 X - 继续迭代旧字典值???
  4. 案例 C 和方法 Y - 仅迭代更新的值如预期!

foreach 循环是否会拍摄静态 Dictionary 成员的某种内部快照?如果是这样,CASE A 也应该工作,但它没有。
有人可以解释导致这种奇怪行为的原因吗?

编辑:

尽管使用了 ConcurrentDictionary 和代码锁定,但基本上我的问题是关于字典更新方法之间的差异:将字典的新副本分配为全新对象或更好地迭代某些集合并使用 dict[key]= 单独更新新值我的对象方法?我不需要保留对值对象的引用,只需替换它。

【问题讨论】:

  • 简单的答案是,您的代码不是线程安全的。如果你想编写线程安全的代码,你真的应该在 ReadDict 和 UpdateDict 方法内容周围使用一个锁块。否则,行为真的是不可预测的 (msdn.microsoft.com/en-us/library/c5kehkcz.aspx)。

标签: c# .net dictionary iteration


【解决方案1】:

字典不是线程安全的。对于线程安全的字典,使用

ConcurrentDictionary<int, string>

可从命名空间 System.Collections.Concurrent 获得;

请参阅以下关于并发字典的优秀教程。

http://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/

编辑:

我认为问题的一部分是即使使用并发字典,您仍然不是线程安全的,因为您的更新方法没有使用并发字典线程安全操作方法。以下尊重集合上的锁。

private static void UpdateDict()
{
    for (int i = 0; i < 50; i++)
    {
         _dict.AddOrUpdate(i, _ => "SECOND", (i1, s) => "SECOND");
    }    
}

您应该会发现这会保留字典。我在索引器上有错字。现在可以按照您的预期正确读取。

【讨论】:

  • 我认为这还不够。他还在 UpdateDict 中制作字典的副本,因此如果两个线程同时访问该代码,您可能会遇到问题。如果他只使用一本字典,您的解决方案可能没问题。这就是为什么我建议使用锁定块(请参阅关于 OP 问题的 cmets)。
  • 您仍然可以将锁与并发字典结合使用,它在指定并发保护级别方面提供了很大的灵活性。最后,根据文章 - “写入字典使用多个锁定对象(也称为细粒度锁定),这大大减少了争用的机会。”
  • 好的,假设我利用 ConcurrentDictionary 并利用它,但无论如何要确定 Read 块是否在新值上迭代,我必须在该块上使用 locker 吗?我只是惊讶于多个同时线程可以从同一个静态成员中读取不同的数据。
  • @Arvis stackoverflow.com/questions/3037637/… 可能有一些答案可以缓解您的惊讶。我最好的猜测是,虽然它是静态的,但每个线程都在操作它认为是当前快照的内容,然后同步回主线程,尽管这只是一个最好的猜测。
  • @Arvis 我的最新更新包括对字典的线程安全更新,它会按照您的预期输出。
猜你喜欢
  • 1970-01-01
  • 2017-10-17
  • 1970-01-01
  • 2016-03-24
  • 2013-04-03
  • 1970-01-01
  • 2020-04-08
  • 2012-07-16
  • 1970-01-01
相关资源
最近更新 更多