lock español c# asp.net .net multithreading access-synchronization

c# - español - ¿Cómo sincronizar el acceso a una Lista<T> utilizada en ASP.NET?



lock c# español (4)

Entonces, ¿esta lista en memoria se comparte entre las solicitudes? Eso suena como una posible causa de problemas incluso cuando tienes bloqueo. Generalmente se deben evitar las colecciones compartidas mutables, IME.

Tenga en cuenta que si decide sincronizar, tendrá que hacer cosas como bloquear la lista para todo el curso de iteración sobre ella y otras operaciones compuestas. Solo bloquear cada acceso específico no es lo suficientemente bueno.

Tengo algunos problemas en un sitio con el acceso simultáneo a una lista. Esta lista mantiene un carrito de artículos y múltiples eliminaciones están bloqueando el sitio. ¿Cuál es el mejor método para sincronizarlos? ¿Es suficiente un candado? La opción de bloqueo parece ser fea porque el código se extiende por todas partes y es bastante desordenado.

Actualización: Esta es una lista implementada de esta manera: public class MyList: List <SomeCustomType> {}

Este es un sitio heredado, por lo que no se permiten tantas modificaciones. ¿Cómo debería refactorizar esto para poder bloquear de forma segura al iterar sobre él?

¡alguna idea!


Sí, tiene que bloquear, usando List.SyncRoot como este:

lock (myList.SyncRoot) { // Access the collection. }


Usar el bloqueo es la forma correcta de sincronizar el acceso a colecciones genéricas. el acceso a la colección se extiende por todas partes, puede crear una clase contenedora para ajustar el acceso a la colección de modo que la superficie de interacción se reduzca. Luego puede introducir la sincronización dentro del objeto de envoltura.


@Samuel Ese es exactamente el punto: NO se puede corregir un problema como este simplemente modificando la clase existente. No importa que la class extends List<T> , lo que de acuerdo con las formas MS evita principalmente los métodos virtuales (solo cuando MS nos propone anularlo, lo hacen virtual, lo cual tiene sentido pero puede dificultar la vida en situaciones en las que necesitas algo un truco). Incluso si incorporaba la List<T> y todos los accesos a ella pasaban por el código que puede cambiar, no podría simplemente modificar este código agregando bloqueos para resolver el problema.

¿Por qué? Bueno, iteradores por una cosa. Una colección no puede simplemente no modificarse entre cada acceso a la colección. No puede modificarse durante toda la enumeración.

En realidad, la sincronización es un asunto complejo y depende de lo que realmente es la lista y de qué tipo de cosas deben ser verdaderas en todo momento en un sistema.

Para ilustrar, aquí hay un hack que abusa de IDisposable. Esto integra la lista y redeclara cualquier funcionalidad de la lista que se use en alguna parte, de modo que el acceso a la lista se pueda sincronizar. Además, no implementa IEnumerable. En cambio, la única forma de obtener acceso para enumerar sobre la lista es a través de un método que devuelve un tipo desechable. Este tipo ingresa al monitor cuando se crea y sale de nuevo cuando se desecha. Esto asegura que la lista no sea accesible durante la iteración. Sin embargo, esto todavía no es suficiente, como lo ilustrará mi ejemplo de uso.

Primero la lista pirateada:

public class MyCollection { object syncRoot = new object(); List list = new List();

public void Add(T item) { lock (syncRoot) list.Add(item); } public int Count { get { lock (syncRoot) return list.Count; } } public IteratorWrapper GetIteratorWrapper() { return new IteratorWrapper(this); } public class IteratorWrapper : IDisposable, IEnumerable<T> { bool disposed; MyCollection<T> c; public IteratorWrapper(MyCollection<T> c) { this.c = c; Monitor.Enter(c.syncRoot); } public void Dispose() { if (!disposed) Monitor.Exit(c.syncRoot); disposed = true; } public IEnumerator<T> GetEnumerator() { return c.list.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } }

}

Luego, una aplicación de consola que lo usa:

class Program { static MyCollection strings = new MyCollection();

static void Main(string[] args) { new Thread(adder).Start(); Thread.Sleep(15); dump(); Thread.Sleep(125); dump(); Console.WriteLine("Press any key."); Console.ReadKey(true); } static void dump() { Console.WriteLine(string.Format("Count={0}", strings.Count).PadLeft(40, ''-'')); using (var enumerable = strings.GetIteratorWrapper()) { foreach (var s in enumerable) Console.WriteLine(s); } Console.WriteLine("".PadLeft(40, ''-'')); } static void adder() { for (int i = 0; i < 100; i++) { strings.Add(Guid.NewGuid().ToString("N")); Thread.Sleep(7); } }

}

Tenga en cuenta el método de "volcado": accede al recuento, que bloquea la lista sin sentido en un intento de hacerlo "seguro para subprocesos" y luego recorre los elementos. Pero existe una condición de carrera entre el contador de contabilización (que bloquea, obtiene el recuento, luego libera) y la declaración de uso. Por lo tanto, no puede volcar el número de elementos que hace.

Aquí, puede no importar. Pero ¿y si el código en cambio hiciera algo como esto?

var a = new string[strings.Count]; for (int i=0; i < strings.Count; i++) { ... }

O incluso más probable que estropee las cosas:

var n = strings.Count; var a = new string[n]; for (int i=0; i < n; i++) { ... }

El primero explotará si los artículos se agregan simultáneamente a la lista. El último no está bajando bien si los artículos se eliminan de la lista. Y en ambos casos, la semántica del código puede no verse afectada por los cambios en la lista, incluso si los cambios no hacen que el código se cuelgue. En el primer caso, tal vez los elementos se eliminan de la lista, lo que hace que la matriz no se llene. Posteriormente, algo muy alejado en el código falla debido a valores nulos en la matriz.

La lección a tomar de esto es: el estado compartido puede ser muy complicado. Necesita un plan inicial y necesita tener una estrategia de cómo va a garantizar que el significado sea el correcto.

En cada uno de estos casos, la operación correcta se lograría solo asegurándose de que el bloqueo abarque todas las operaciones relacionadas. Podría haber muchas otras declaraciones intermedias y el bloqueo podría abarcar muchas llamadas a métodos y / o involucrar muchos objetos diferentes. No existe una solución mágica que permita solucionar estos problemas simplemente modificando la colección en sí, ya que la sincronización correcta depende de lo que signifique la recopilación. Algo así como un caché de objetos de solo lectura probablemente solo se pueda sincronizar con respecto a add/remove/lookup , pero si se supone que la colección en sí representa algún concepto significativo / importante, esto nunca será suficiente.