Могу ли я удалить элементы из ConcurrentDictionary из цикла перечисления этого словаря? - PullRequest
31 голосов
/ 23 февраля 2010

Так, например:

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 ]

33 голосов
/ 28 апреля 2010

Мне странно, что вы получили два ответа, которые, кажется, подтверждают, что вы не можете этого сделать. Я только что проверил это сам, и он работал нормально, без каких-либо исключений.

Ниже приведен код, который я использовал для проверки поведения, за которым следует выдержка из выходных данных (примерно, когда я нажал «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».)

15 голосов
/ 05 марта 2012

Просто чтобы подтвердить, что официальная документация прямо заявляет, что это безопасно:

Возвращенный из словаря перечислитель безопасен в использовании одновременно с чтением и записью в словарь, однако это не представляют моментальный снимок словаря. содержимое, открываемое через перечислитель, может содержать изменения, сделанные в словарь после вызова GetEnumerator.

1 голос
/ 12 июня 2010

Дополнительную информацию об этом поведении можно найти здесь:

Блог MSDN

Отрывок:

  • Самым большим изменением является то, что мы перебираем то, что возвращается свойством «Ключи», которое возвращает снимок ключей в словаре в заданной точке. Это означает, что последующие изменения словаря не повлияют на цикл, так как он работает со снимком. Не вдаваясь в подробности, перебор самой коллекции имеет совершенно другое поведение, которое может позволить последующим модификациям быть включенными в цикл; это делает его менее детерминированным.
  • Если элементы добавляются другими потоками после начала цикла, они будут сохранены в коллекции, но они не будут включены в эту операцию обновления (увеличивая свойства счетчика).
  • Если элемент удален другим потоком до вызова TryGetValue, вызов завершится неудачно и ничего не произойдет. Если элемент удален после вызова TryGetValue, "tmp.
0 голосов
/ 28 апреля 2010

Редактировать, после проверки решения Дэна Тао и независимого тестирования.

Да, это короткий ответ. Это не будет, за исключением того, что он, кажется, использует мелкозернистую блокировку, и работает как рекламируется.

Bob.

...