threading thread que practice net method lock ejemplo best c# .net multithreading thread-synchronization

c# - thread - Mecanismo de sincronización para un objeto observable.



synchronized c# method (3)

Imaginemos que tenemos que sincronizar el acceso de lectura / escritura a los recursos compartidos. Múltiples hilos accederán a ese recurso tanto en lectura como en escritura (la mayoría de las veces para leer, a veces para escribir). Supongamos también que cada escritura siempre activará una operación de lectura (el objeto es observable).

Para este ejemplo, me imagino una clase como esta (perdone la sintaxis y el estilo, solo con fines ilustrativos):

class Container { public ObservableCollection<Operand> Operands; public ObservableCollection<Result> Results; }

Estoy tentado a usar un ReadWriterLockSlim para este propósito, además lo pondría a nivel de Container (imagina que el objeto no es tan simple y una operación de lectura / escritura puede involucrar varios objetos):

public ReadWriterLockSlim Lock;

La implementación de Operand y Result no tiene significado para este ejemplo. Ahora imaginemos un código que observe los Operands y produzca un resultado para poner en Results :

void AddNewOperand(Operand operand) { try { _container.Lock.EnterWriteLock(); _container.Operands.Add(operand); } finally { _container.ExitReadLock(); } }

Nuestro observador hipotético hará algo similar pero consumirá un nuevo elemento y se bloqueará con EnterReadLock() para obtener operandos y luego EnterWriteLock() para agregar un resultado (permítame omitir el código para esto). Esto producirá una excepción debido a la recursión, pero si configuro LockRecursionPolicy.SupportsRecursion , solo abriré mi código en LockRecursionPolicy.SupportsRecursion muertos (de MSDN ):

De forma predeterminada, las nuevas instancias de ReaderWriterLockSlim se crean con el indicador LockRecursionPolicy.NoRecursion y no permiten la recursión. Esta política predeterminada se recomienda para todos los desarrollos nuevos, porque la recursión introduce complicaciones innecesarias y hace que su código sea más propenso a los interbloqueos .

Repito parte relevante para mayor claridad:

La recursión [...] hace que su código sea más propenso a los interbloqueos.

Si no estoy equivocado con LockRecursionPolicy.SupportsRecursion si desde el mismo hilo pido un, digamos, lea el bloqueo, entonces alguien más pide un bloqueo de escritura, entonces tendré un bloqueo de bloqueo, entonces lo que dice MSDN tiene sentido. Además, la recursión degradará el rendimiento también de manera mensurable (y no es lo que quiero si ReadWriterLockSlim lugar de ReadWriterLock o Monitor ).

Pregunta (s)

Finalmente, mis preguntas son (tenga en cuenta que no estoy buscando una discusión sobre los mecanismos generales de sincronización, sabría lo que está mal para este escenario productor / observable / observador ):

  • ¿Qué es mejor en esta situación? ¿Para evitar ReadWriterLockSlim en favor de Monitor (incluso si en el mundo real las lecturas serán mucho más que escrituras)?
  • ¿Abandonar con tan gruesa sincronización? Esto puede incluso producir un mejor rendimiento, pero hará que el código sea mucho más complicado (por supuesto, no en este ejemplo sino en el mundo real).
  • ¿Debo hacer notificaciones (de la colección observada) de forma asíncrona?
  • ¿Algo más que no pueda ver?

Sé que no hay un mejor mecanismo de sincronización, por lo que la herramienta que usamos debe ser la correcta para nuestro caso, pero me pregunto si hay alguna práctica recomendada o simplemente ignoro algo muy importante entre los hilos y los observadores (imagínese usar las extensiones reactivas de Microsoft, pero la pregunta es general, no está ligado a ese marco).

¿Soluciones posibles?

Lo que intentaría es hacer que los eventos (de alguna manera) sean diferidos:

1ª solución
Cada cambio no activará ningún evento CollectionChanged , se mantiene en una cola. Cuando el proveedor (el objeto que empuja datos) haya finalizado, forzará manualmente la cola para que se vacíe (elevando cada evento en secuencia). Esto se puede hacer en otro hilo o incluso en el hilo de la persona que llama (pero fuera del bloqueo).

Puede funcionar, pero hará que todo sea menos "automático" (cada notificación de cambio debe ser activada manualmente por el propio productor, más código para escribir, más errores por todos lados).

2da solución
Otra solución puede ser proporcionar una referencia a nuestro bloqueo a la colección observable. Si envuelvo ReadWriterLockSlim en un objeto personalizado (útil para esconderlo en un objeto IDisposable fácil de usar), puedo agregar un ManualResetEvent para notificar que todos los bloqueos se han liberado de esta manera. La misma colección puede generar eventos (nuevamente en el mismo hilo o en otro hilo).

3ra solución
Otra idea podría ser simplemente hacer eventos asíncronos. Si el controlador de eventos necesitará un bloqueo, entonces se detendrá para esperar su período de tiempo. Para esto me preocupa la cantidad de subprocesos grande que se puede usar (especialmente si se trata de un grupo de subprocesos).

Sinceramente, no sé si alguno de estos es aplicable en aplicaciones del mundo real (personalmente, desde el punto de vista de los usuarios, prefiero el segundo, pero implica una colección personalizada para todo y hace que la colección esté al tanto de los hilos y lo evitaría, si posible). No me gustaría hacer el código más complicado de lo necesario.


Esto suena como bastante el pickle multihilo. Es bastante difícil trabajar con la recursión en este patrón de cadena de eventos, mientras se evitan los interbloqueos. Es posible que desee considerar el diseño alrededor del problema por completo.

Por ejemplo, podría hacer que la adición de un operando sea asíncrona a la aparición del evento:

private readonly BlockingCollection<Operand> _additions = new BlockingCollection<Operand>(); public void AddNewOperand(Operand operand) { _additions.Add(operand); }

Y luego hacer que la adición real suceda en un hilo de fondo:

private void ProcessAdditions() { foreach(var operand in _additions.GetConsumingEnumerable()) { _container.Lock.EnterWriteLock(); _container.Operands.Add(operand); _container.Lock.ExitWriteLock(); } } public void Initialize() { var pump = new Thread(ProcessAdditions) { Name = "Operand Additions Pump" }; pump.Start(); }

Esta separación sacrifica algo de coherencia: el código que se ejecuta después del método de adición realmente no sabrá cuándo se produjo la adición y tal vez eso sea un problema para su código. Si es así, esto podría reescribirse para suscribirse a la observación y usar una Task para indicar cuándo se completa el agregado:

public Task AddNewOperandAsync(Operand operand) { var tcs = new TaskCompletionSource<byte>(); // Compose an event handler for the completion of this task NotifyCollectionChangedEventHandler onChanged = null; onChanged = (sender, e) => { // Is this the event for the operand we have added? if (e.NewItems.Contains(operand)) { // Complete the task. tcs.SetCompleted(0); // Remove the event-handler. _container.Operands.CollectionChanged -= onChanged; } } // Hook in the handler. _container.Operands.CollectionChanged += onChanged; // Perform the addition. _additions.Add(operand); // Return the task to be awaited. return tcs.Task; }

La lógica del controlador de eventos se activa en el subproceso de fondo que bombea los mensajes de adición, por lo que no hay posibilidad de que bloquee los subprocesos de primer plano. Si espera la adición en el mensaje-pump para la ventana, el contexto de sincronización es lo suficientemente inteligente como para programar la continuación en el hilo del mensaje-pump también.

Ya sea que siga la ruta de la Task o no, esta estrategia significa que puede agregar más operandos de forma segura desde un evento observable sin tener que volver a ingresar ningún bloqueo.


No estoy seguro de si este es exactamente el mismo problema, pero al tratar con cantidades relativamente pequeñas de datos (entradas de 2k a 3k), he usado el siguiente código para facilitar el acceso de lectura / escritura de subprocesos múltiples a las colecciones vinculadas a la IU. Este código se encontró originalmente here .

public class BaseObservableCollection<T> : ObservableCollection<T> { // Constructors public BaseObservableCollection() : base() { } public BaseObservableCollection(IEnumerable<T> items) : base(items) { } public BaseObservableCollection(List<T> items) : base(items) { } // Evnet public override event NotifyCollectionChangedEventHandler CollectionChanged; // Event Handler protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { // Be nice - use BlockReentrancy like MSDN said using (BlockReentrancy()) { if (CollectionChanged != null) { // Walk thru invocation list foreach (NotifyCollectionChangedEventHandler handler in CollectionChanged.GetInvocationList()) { DispatcherObject dispatcherObject = handler.Target as DispatcherObject; // If the subscriber is a DispatcherObject and different thread if (dispatcherObject != null && dispatcherObject.CheckAccess() == false) { // Invoke handler in the target dispatcher''s thread dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, e); } else { // Execute handler as is handler(this, e); } } } } } }

También he usado el siguiente código (que se hereda del código anterior) para admitir el evento CollectionChanged cuando los elementos dentro de la colección generan el PropertyChanged .

public class BaseViewableCollection<T> : BaseObservableCollection<T> where T : INotifyPropertyChanged { // Constructors public BaseViewableCollection() : base() { } public BaseViewableCollection(IEnumerable<T> items) : base(items) { } public BaseViewableCollection(List<T> items) : base(items) { } // Event Handlers private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e) { var arg = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender); base.OnCollectionChanged(arg); } protected override void ClearItems() { foreach (T item in Items) { if (item != null) { item.PropertyChanged -= ItemPropertyChanged; } } base.ClearItems(); } protected override void InsertItem(int index, T item) { if (item != null) { item.PropertyChanged += ItemPropertyChanged; } base.InsertItem(index, item); } protected override void RemoveItem(int index) { if (Items[index] != null) { Items[index].PropertyChanged -= ItemPropertyChanged; } base.RemoveItem(index); } protected override void SetItem(int index, T item) { if (item != null) { item.PropertyChanged += ItemPropertyChanged; } base.SetItem(index, item); } }


Sincronización de la colección de hilos cruzados

Al colocar un enlace de ListBox en un ObservableCollection , cuando los datos cambian, se actualiza el ListBox porque se implementó INotifyCollectionChanged. El defecto dell''ObservableCollection es que los datos solo pueden ser modificados por el hilo que los creó.

SynchronizedCollection no tiene el problema de Multi-Thread pero no actualiza el ListBox porque no se implementa INotifyCollectionChanged, incluso si implementa INotifyCollectionChanged, CollectionChanged (this, e) solo se puede llamar desde el thread que lo creó ... así que No funciona.

Conclusión

-Si desea una lista que se actualiza automáticamente, use el subproceso ObservableCollection

-Si desea una lista que no se actualice automáticamente pero que sea multiproceso, use SynchronizedCollection

- Si desea ambos, use Framework 4.5 , BindingOperations.EnableCollectionSynchronization y ObservableCollection () de esta manera:

/ / Creates the lock object somewhere private static object _lock = new object () ; ... / / Enable the cross acces to this collection elsewhere BindingOperations.EnableCollectionSynchronization ( _persons , _lock )

La muestra completa http://10rem.net/blog/2012/01/20/wpf-45-cross-thread-collection-synchronization-redux