代码之家  ›  专栏  ›  技术社区  ›  redcalx

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

  •  29
  • redcalx  · 技术社区  · 14 年前

    例如:

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

    显然,对于普通字典,这将引发异常,因为移除项会在枚举期间更改字典的内部状态。据我所知,这不是ConcurrentDictionary的情况,因为提供的IEnumerable处理内部状态更改。我理解这一点对吗?是否有更好的模式可供使用?

    4 回复  |  直到 8 年前
        1
  •  31
  •   Dan Tao    14 年前

    我很奇怪,你现在收到了两个答案,似乎证实你不能这样做。我自己测试过它,它运行良好,没有任何异常。

    下面是我用来测试行为的代码,后面是输出的摘录(当我按下“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
    

    还要注意,根据控制台输出,它看起来像 前额 循环锁定了试图向字典添加值的其他线程。(我可能是错的,但如果不是这样,我猜你会看到在“count”行之间有一堆“added entry”行。)

        2
  •  14
  •   Sergey Vyacheslavovich Brunov prodigitalson    8 年前

    只是确认一下 the official documentation 明确声明它是安全的:

    从字典返回的枚举器可以安全使用 同时读写字典,但是它 不代表字典的时间快照。这个 通过枚举器公开的内容可能包含所做的修改 在调用GetEnumerator之后转到字典。

        3
  •  2
  •   Sergey Vyacheslavovich Brunov prodigitalson    8 年前

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

    MSDN Blog

    片段:

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

    编辑后,检查丹桃溶液并独立测试。

    是的,是简短的回答。它不会例外,它看起来确实使用了细粒度的锁,并且可以像广告中那样工作。

    鲍勃。