c# - Bloqueo frente a ToArray para acceso seguro para subprocesos de la colección de listas
multithreading foreach (5)
Tengo una colección de listas y quiero recorrerla en una aplicación de varios subprocesos. Necesito protegerlo cada vez que lo itero ya que se podría cambiar y no quiero excepciones de "colección modificada" cuando hago un foreach
¿Cuál es la forma correcta de hacer esto?
Utilizar el bloqueo cada vez que accedo o bucle. Estoy bastante aterrorizado de los puntos muertos. Tal vez estoy paranoico de usar el bloqueo y no debería serlo. ¿Qué necesito saber si tomo esta ruta para evitar puntos muertos? ¿Es el bloqueo bastante eficiente?
Use List <>. ToArray () para copiar a una matriz cada vez que hago un foreach. Esto causa un impacto en el rendimiento, pero es fácil de hacer. Estoy preocupado por la memoria, así como el tiempo para copiarla. Simplemente parece excesivo. ¿Es seguro usar el hilo de ToArray?
No use foreach y use para bucles en su lugar. ¿No tendría que hacer una verificación de longitud cada vez que hice esto para asegurarme de que la lista no se redujera? Eso parece molesto.
En general, las colecciones no son seguras para subprocesos por razones de rendimiento, excepto en Hash Table. Tienes que usar IsSynchronized y SyncRoot para hacer que los subprocesos sean seguros. Ver here y here
Ejemplo de msdn
ICollection myCollection = someCollection;
lock(myCollection.SyncRoot)
{
foreach (object item in myCollection)
{
// Insert your code here.
}
}
Editar: Si está utilizando .net 4.0, puede usar colecciones concurrentes
Hay pocas razones para tener miedo de los puntos muertos, son fáciles de detectar. Su programa deja de funcionar, sorteo muerto. De lo que realmente deberías estar aterrado es en las carreras de subprocesos, el tipo de error que obtendrás cuando no te bloquees cuando deberías. Muy difícil de diagnosticar.
Usar el bloqueo está bien, solo asegúrate de usar exactamente el mismo objeto de bloqueo en cualquier código que toque esa lista. Como el código que agrega o elimina elementos de esa lista. Si ese código se ejecuta en el mismo hilo que itera la lista, entonces no necesita un bloqueo. En general, la única posibilidad de interbloqueo aquí es si tiene un código que se basa en el estado del hilo, como Thread.Join (), mientras que también mantiene ese objeto de bloqueo. Lo que debería ser raro.
Sí, iterar una copia de la lista siempre es seguro para subprocesos, siempre que use un bloqueo alrededor del método ToArray (). Tenga en cuenta que todavía necesita el bloqueo, no hay mejoras estructurales. La ventaja es que mantendrá el bloqueo durante un corto período de tiempo, mejorando la concurrencia en su programa. Las desventajas son sus requisitos de almacenamiento O (n), que solo tienen una lista segura pero no protegen los elementos de la lista y el problema difícil de tener siempre una vista obsoleta del contenido de la lista. Especialmente el último problema es sutil y difícil de analizar. Si no puede razonar los efectos secundarios, entonces probablemente no debería considerar esto.
Asegúrese de tratar la capacidad de Foreach para detectar una raza como un regalo, no un problema. Sí, un bucle explícito para (;;) no va a lanzar la excepción, solo va a funcionar mal. Me gusta iterar el mismo elemento dos veces o omitir un elemento por completo. Podría evitar tener que volver a verificar el número de elementos iterándolo hacia atrás. Siempre que otros subprocesos solo estén llamando a Add () y no a Remove () que se comportarían de manera similar a ToArray (), obtendrás la vista obsoleta. No es que esto funcione en la práctica, indexar la lista tampoco es seguro para subprocesos. Lista <> reasignará su matriz interna si es necesario. Esto simplemente no funcionará y funcionará mal de manera impredecible.
Hay dos puntos de vista aquí. Puedes estar aterrorizado y seguir la sabiduría común, obtendrás un programa que funciona pero puede que no sea óptimo. Eso es sabio y mantiene feliz al jefe. O puede experimentar y descubrir por sí mismo cómo el sesgo de las reglas lo mete en problemas. Lo que te hará feliz, serás mucho mejor programador. Pero tu productividad va a sufrir. No sé cómo es tu horario.
Si los datos de su lista son en su mayoría de solo lectura, puede permitir que múltiples subprocesos accedan a ellos de manera segura y simultáneamente utilizando un ReaderWriterLockSlim
Puede encontrar una implementación de un diccionario seguro para subprocesos aquí para comenzar.
También quería mencionar que si está utilizando .Net 4.0, la clase BlockingCollection implementa esta funcionalidad automáticamente. ¡Ojalá hubiera sabido de esto hace unos meses!
También podría considerar el uso de una estructura de datos inmutable; trate su lista como un tipo de valor.
Si es posible, usar objetos inmutables puede ser una excelente opción para la programación de subprocesos múltiples porque eliminan toda la semántica de bloqueo. Esencialmente, cualquier operación que cambie el estado del objeto crea un objeto completamente nuevo.
Por ejemplo, preparé lo siguiente para demostrar la idea. Me disculparé por no ser un código de referencia, y comenzó a ser un poco largo.
public class ImmutableWidgetList : IEnumerable<Widget>
{
private List<Widget> _widgets; // we never modify the list
// creates an empty list
public ImmutableWidgetList()
{
_widgets = new List<Widget>();
}
// creates a list from an enumerator
public ImmutableWidgetList(IEnumerable<Widget> widgetList)
{
_widgets = new List<Widget>(widgetList);
}
// add a single item
public ImmutableWidgetList Add(Widget widget)
{
List<Widget> newList = new List<Widget>(_widgets);
ImmutableWidgetList result = new ImmutableWidgetList();
result._widgets = newList;
return result;
}
// add a range of items.
public ImmutableWidgetList AddRange(IEnumerable<Widget> widgets)
{
List<Widget> newList = new List<Widget>(_widgets);
newList.AddRange(widgets);
ImmutableWidgetList result = new ImmutableWidgetList();
result._widgets = newList;
return result;
}
// implement IEnumerable<Widget>
IEnumerator<Widget> IEnumerable<Widget>.GetEnumerator()
{
return _widgets.GetEnumerator();
}
// implement IEnumerable
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _widgets.GetEnumerator();
}
}
-
IEnumerable<T>
para permitir una implementaciónforeach
. - Mencionó que estaba preocupado por el rendimiento en espacio / tiempo de la creación de nuevas listas, por lo que quizás esto no le funcione.
- es posible que también desee implementar
IList<T>
Use lock()
menos que tenga otra razón para hacer copias. Los puntos muertos solo pueden ocurrir si solicita varios bloqueos en diferentes órdenes, por ejemplo:
Hilo 1:
lock(A) {
// .. stuff
// Next lock request can potentially deadlock with 2
lock(B) {
// ... more stuff
}
}
Hilo 2:
lock(B) {
// Different stuff
// next lock request can potentially deadlock with 1
lock(A) {
// More crap
}
}
Aquí, el subproceso 1 y el subproceso 2 tienen el potencial de causar un interbloqueo ya que el subproceso 1 puede contener A
mientras que el subproceso 2 mantiene B
y ninguno puede continuar hasta que el otro libere su bloqueo.
Si debe realizar varios bloqueos, hágalo siempre en el mismo orden. Si solo está utilizando un bloqueo, no provocará un interbloqueo ... a menos que mantenga un bloqueo mientras espera la entrada del usuario, pero técnicamente no es un interbloqueo y lleva a otro punto: nunca más retenga un bloqueo. que absolutamente debes