【问题标题】:Can I remove items from a ConcurrentDictionary from within an enumeration loop of that dictionary?我可以从该字典的枚举循环中从 ConcurrentDictionary 中删除项目吗?
【发布时间】:2011-01-20 01:17:13
【问题描述】:

例如:

ConcurrentDictionary<string,Payload> itemCache = GetItems();

foreach(KeyValuePair<string,Payload> kvPair in itemCache)
{
    if(TestItemExpiry(kvPair.Value))
    {   // Remove expired item.
        itemCache.TryRemove(kvPair.Key, out Payload removedItem);
    }
}

显然对于普通的 Dictionary 这将抛出异常,因为删除项目会在枚举的生命周期内改变字典的内部状态。我的理解是 ConcurrentDictionary 不是这种情况,因为提供的 IEnumerable 处理内部状态更改。我理解对了吗?有没有更好的模式可以使用?

【问题讨论】:

    标签: .net concurrency parallel-extensions


    【解决方案1】:

    让我感到奇怪的是,您现在收到了两个似乎证实您不能这样做的答案。我刚刚自己测试过,它运行良好,没有抛出任何异常。

    下面是我用来测试行为的代码,然后是输出的摘录(大约在我按下“C”以清除 foreachS 中的字典以停止后台线程时) .请注意,我对ConcurrentDictionary 施加了相当大的压力:16 个线程计时器,每个线程计时器大约每 15 毫秒尝试添加一个项目。

    在我看来,这个类非常健壮,如果您在多线程场景中工作,则值得您关注。

    代码

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Threading;
    
    namespace ConcurrencySandbox {
        class Program {
            private const int NumConcurrentThreads = 16;
            private const int TimerInterval = 15;
    
            private static ConcurrentDictionary<int, int> _dictionary;
            private static WaitHandle[] _timerReadyEvents;
            private static Timer[] _timers;
            private static volatile bool _timersRunning;
    
            [ThreadStatic()]
            private static Random _random;
            private static Random GetRandom() {
                return _random ?? (_random = new Random());
            }
    
            static Program() {
                _dictionary = new ConcurrentDictionary<int, int>();
                _timerReadyEvents = new WaitHandle[NumConcurrentThreads];
                _timers = new Timer[NumConcurrentThreads];
    
                for (int i = 0; i < _timerReadyEvents.Length; ++i)
                    _timerReadyEvents[i] = new ManualResetEvent(true);
    
                for (int i = 0; i < _timers.Length; ++i)
                    _timers[i] = new Timer(RunTimer, _timerReadyEvents[i], Timeout.Infinite, Timeout.Infinite);
    
                _timersRunning = false;
            }
    
            static void Main(string[] args) {
                Console.Write("Press Enter to begin. Then press S to start/stop the timers, C to clear the dictionary, or Esc to quit.");
                Console.ReadLine();
    
                StartTimers();
    
                ConsoleKey keyPressed;
                do {
                    keyPressed = Console.ReadKey().Key;
                    switch (keyPressed) {
                        case ConsoleKey.S:
                            if (_timersRunning)
                                StopTimers(false);
                            else
                                StartTimers();
    
                            break;
                        case ConsoleKey.C:
                            Console.WriteLine("COUNT: {0}", _dictionary.Count);
                            foreach (var entry in _dictionary) {
                                int removedValue;
                                bool removed = _dictionary.TryRemove(entry.Key, out removedValue);
                            }
                            Console.WriteLine("COUNT: {0}", _dictionary.Count);
    
                            break;
                    }
    
                } while (keyPressed != ConsoleKey.Escape);
    
                StopTimers(true);
            }
    
            static void StartTimers() {
                foreach (var timer in _timers)
                    timer.Change(0, TimerInterval);
    
                _timersRunning = true;
            }
    
            static void StopTimers(bool waitForCompletion) {
                foreach (var timer in _timers)
                    timer.Change(Timeout.Infinite, Timeout.Infinite);
    
                if (waitForCompletion) {
                    WaitHandle.WaitAll(_timerReadyEvents);
                }
    
                _timersRunning = false;
            }
    
            static void RunTimer(object state) {
                var readyEvent = state as ManualResetEvent;
                if (readyEvent == null)
                    return;
    
                try {
                    readyEvent.Reset();
    
                    var r = GetRandom();
                    var entry = new KeyValuePair<int, int>(r.Next(), r.Next());
                    if (_dictionary.TryAdd(entry.Key, entry.Value))
                        Console.WriteLine("Added entry: {0} - {1}", entry.Key, entry.Value);
                    else
                        Console.WriteLine("Unable to add entry: {0}", entry.Key);
    
                } finally {
                    readyEvent.Set();
                }
            }
        }
    }
    

    输出(摘录)

    cAdded entry: 108011126 - 154069760   // <- pressed 'C'
    Added entry: 245485808 - 1120608841
    Added entry: 1285316085 - 656282422
    Added entry: 1187997037 - 2096690006
    Added entry: 1919684529 - 1012768429
    Added entry: 1542690647 - 596573150
    Added entry: 826218346 - 1115470462
    Added entry: 1761075038 - 1913145460
    Added entry: 457562817 - 669092760
    COUNT: 2232                           // <- foreach loop begins
    COUNT: 0                              // <- foreach loop ends
    Added entry: 205679371 - 1891358222
    Added entry: 32206560 - 306601210
    Added entry: 1900476106 - 675997119
    Added entry: 847548291 - 1875566386
    Added entry: 808794556 - 1247784736
    Added entry: 808272028 - 415012846
    Added entry: 327837520 - 1373245916
    Added entry: 1992836845 - 529422959
    Added entry: 326453626 - 1243945958
    Added entry: 1940746309 - 1892917475
    

    还请注意,根据控制台输出,foreach 循环似乎锁定了其他试图向字典添加值的线程。 (我可能是错的,否则我猜你会在“COUNT”行之间看到一堆“Added entry”行。)

    【讨论】:

    • 很高兴知道迭代器(foreach)是否锁定了其他线程……但我不这么认为……谁知道更多?
    • @Peter Gfader:您查看了错误的文档! ConcurrentQueue 迭代快照; ConcurrentDictionary 确实msdn.microsoft.com/en-us/library/dd287131(v=VS.100).aspx
    • 实际上,查看 mscorlib 的反汇编,Version=4.0.0.0(据我所知是最新的),它绝对没有以任何方式使用 Keys 的快照。迭代器似乎正在通过枚举器的 MoveNext 方法中每个节点的 m_next 属性对存储在“m_buckets”中的持久节点数组进行迭代......这似乎表明任何新的键都会在最后被拾取。文档不清楚。
    • 好的,删除这最后几个cmets。我已经把事情清理干净了。反汇编是正确的,它不会遍历 snapshop,此后没有任何改变。 'rboardman' 指的是一个线程,其中通过显式迭代返回只读快照的'Keys' 属性在他们自己的代码中进行了“更改”。
    • @Triynko:哈,感谢您的调查。老实说,我实际上是持怀疑态度的——我似乎记得我的测试表明它没有在快照上迭代,而一个月对于 BCL 的改变来说真的很短——但这个答案来自很长一段时间前;而且我什至无法轻松访问 Windows 机器来测试它!所以我不确定该相信什么。不管怎样,听起来你现在已经想通了。
    【解决方案2】:

    只是为了确认the official documentation 明确声明它是安全的:

    从字典返回的枚举器可以安全使用 与字典的读取和写入同时进行,但确实如此 不代表字典的即时快照。这 通过枚举器暴露的内容可能包含所做的修改 调用 GetEnumerator 后到字典中。

    【讨论】:

    • “可能包含”和“对字典进行的修改”的含义并不完全清楚。字典可能发生两种不同的修改,其中只有一种在功能上(但不是原子上)与枚举器相关:1.更新集合中的现有值,2.添加或删除集合中的键.我不希望第一种类型的更改会干扰并发字典,当您访问它时,您只会获得每个键的最新值。然而,这很清楚......
    • 如果在枚举过程中添加了新键,枚举器会发生什么?他们说它“可能包含”修改。枚举的顺序是什么?例如,如果在同时添加新键时我已经完成了 99% 的枚举,那么只要在枚举完成之前添加新键,枚举器就可以保证迭代该新键吗?或者,新密钥是否会卡在已经枚举的前 99% 的密钥中的某个位置,这样这个特定的迭代就不会捡起它。枚举算法和排序需要指定。
    • 答案在 MSDN 博客中(不幸的是不在文档中):“最大的变化是我们正在迭代“Keys”属性返回的内容,该属性返回的快照字典中给定点的键。这意味着循环不会受到字典后续修改的影响,因为它是在快照上运行的。"
    • @Triynko 你能给我那个 MSDN 博客文章的链接吗?谢谢!
    • @Triynko 那段代码确实迭代了键,而不是字典。
    【解决方案3】:

    编辑,在检查丹道解决方案并独立测试后。

    是的,这是简短的回答。它不会例外,它似乎确实使用了细粒度的锁定,并且像宣传的那样工作。

    鲍勃。

    【讨论】:

    • 您使用了ConcurrentDictionaryTryRemove?根据我的测试,这应该有效。在您的情况下是否有其他原因导致异常?
    • 嗨,丹,不,我很惭愧地说我认为我没有这样做。我将更改代码,看看它是否例外。如果不是,我将向您的智慧低头并编辑答案以反映您的智慧。鲍勃。
    【解决方案4】:

    可以在此处找到有关此行为的其他信息:

    MSDN Blog

    片段:

    • 最大的变化是我们正在迭代“Keys”属性返回的内容,该属性返回字典中给定点的键的快照。这意味着循环不会受到后续对字典的修改的影响,因为它是在快照上运行的。在不涉及太多细节的情况下,对集合本身的迭代具有微妙的不同行为,可能允许将后续修改包含在循环中;这使得它的确定性降低。
    • 如果项目在循环开始后被其他线程添加,它们将存储在集合中,但不会包含在此更新操作中(递增计数器属性)。
    • 如果一个项目在调用 TryGetValue 之前被另一个线程删除,调用将失败并且不会发生任何事情。如果在调用 TryGetValue 后删除了某个项目,则“tmp.

    【讨论】:

    • 此信息不正确且具有误导性。那篇 MSDN 博客文章指的是用户在自己的代码中所做的“更改”;他们迭代了“Keys”集合(快照)的返回值,而迭代字典本身上的 KeyValuePairs 不使用 snapshop,并且框架中没有任何变化。它仍然使用内部节点列表,该列表可以随时更新,因此在枚举开始后添加的任何新节点可能会或可能不会被拾取,具体取决于枚举的进展程度以及新节点在列表中的插入位置.
    猜你喜欢
    • 1970-01-01
    • 2011-03-09
    • 1970-01-01
    • 2014-06-28
    • 2021-02-20
    • 1970-01-01
    • 2013-04-22
    • 2022-01-23
    • 2011-01-07
    相关资源
    最近更新 更多