.net concurrency parallel-extensions

.net - ¿Puedo eliminar elementos de un ConcurrentDictionary desde un bucle de enumeración de ese diccionario?



concurrency parallel-extensions (4)

Así por ejemplo:

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); } }

Obviamente, con un diccionario ordinario esto arrojará una excepción porque la eliminación de elementos cambia el estado interno del diccionario durante la vida de la enumeración. Tengo entendido que este no es el caso para un ConcurrentDictionary ya que el IEnumerable proporcionado maneja el cambio de estado interno. ¿Estoy entendiendo esto bien? ¿Hay un mejor patrón para usar?


Es extraño para mí que hayas recibido dos respuestas que parecen confirmar que no puedes hacer esto. Acabo de probarlo yo mismo y funcionó bien sin tirar ninguna excepción.

A continuación se muestra el código que usé para probar el comportamiento, seguido de un extracto de la salida (alrededor de cuando presioné ''C'' para borrar el diccionario en un foreach y S inmediatamente después para detener los hilos de fondo). Tenga en cuenta que pongo una cantidad considerable de estrés en este ConcurrentDictionary : 16 temporizadores de subprocesos cada uno tratando de agregar un elemento aproximadamente cada 15 milisegundos.

Me parece que esta clase es bastante robusta y merece su atención si está trabajando en un escenario multiproceso.

Código

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(); } } } }

Salida (extracto)

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

También tenga en cuenta que, en función de la salida de la consola, parece que el bucle foreach bloqueó los otros hilos que intentaban agregar valores al diccionario. (Podría estar equivocado, pero de lo contrario hubiera adivinado que habrías visto un montón de líneas de "entrada agregada" entre las líneas "COUNT").


Editar, después de verificar la solución de Dan Tao y probar de forma independiente.

Sí, es la respuesta corta. No lo hará excepto, parece usar un bloqueo de grano fino, y funciona como se anuncia.

Chelín.


Información adicional sobre este comportamiento se puede encontrar aquí:

Blog de MSDN

Retazo:

  • El mayor cambio es que estamos iterando sobre lo que devuelve la propiedad "Keys", que devuelve una instantánea de las claves en el diccionario en un punto determinado. Esto significa que el ciclo no se verá afectado por modificaciones posteriores en el diccionario, ya que está operando en una instantánea. Sin entrar en demasiados detalles, iterar sobre la colección en sí tiene un comportamiento completamente diferente que puede permitir que las modificaciones posteriores se incluyan en el ciclo; esto lo hace menos determinista.
  • Si los elementos son agregados por otros hilos después de que comience el ciclo, se almacenarán en la colección, pero no se incluirán en esta operación de actualización (incrementando las propiedades del contador).
  • Si un artículo es eliminado por otro hilo antes de la llamada a TryGetValue, la llamada fallará y no pasará nada. Si se elimina un elemento después de la llamada a TryGetValue, el "tmp.

Solo para confirmar que la documentación oficial declara explícitamente que es seguro:

El enumerador devuelto por el diccionario es seguro de usar al mismo tiempo que las lecturas y escrituras en el diccionario, sin embargo, no representa una instantánea de momento en el tiempo del diccionario. Los contenidos expuestos a través del enumerador pueden contener modificaciones realizadas en el diccionario después de llamar a GetEnumerator.