我可以从 ConcurrentDictionary 的枚举循环中删除项目吗?

问题描述 投票:0回答:4

举个例子:

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<K,V>
这会引发异常,因为删除项目会在枚举生命周期内更改字典的内部状态。据我了解,
ConcurrentDictionary
的情况并非如此,因为提供的
IEnumerable
处理内部状态更改。我这样理解对吗?有更好的模式可以使用吗?

.net concurrency concurrentdictionary parallel-extensions
4个回答
42
投票

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

下面是我用来测试行为的代码,后面是输出的摘录(大约是当我按“C”清除

foreach
中的字典,然后立即按
S
停止后台线程时)。请注意,我对此施加了相当大的压力
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”行之间看到一堆“添加条目”行。)


20
投票

只是为了确认官方文档明确指出它是安全的:

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


0
投票

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

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

鲍勃。


-1
投票

有关此行为的更多信息可以在此处找到:

MSDN 博客

片段:

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