¿Cuál es la mejor forma de implementar un Diccionario seguro para subprocesos?

Pude implementar un Diccionario seguro para subprocesos en C # derivando de IDictionary y definiendo un objeto SyncRoot privado:

public class SafeDictionary: IDictionary { private readonly object syncRoot = new object(); private Dictionary d = new Dictionary(); public object SyncRoot { get { return syncRoot; } } public void Add(TKey key, TValue value) { lock (syncRoot) { d.Add(key, value); } } // more IDictionary members... } 

Luego locking este objeto SyncRoot en todos mis consumidores (múltiples hilos):

Ejemplo:

 lock (m_MySharedDictionary.SyncRoot) { m_MySharedDictionary.Add(...); } 

Pude hacer que funcionara, pero esto dio como resultado un código feo. Mi pregunta es, ¿existe una forma mejor y más elegante de implementar un Diccionario seguro para subprocesos?

Como dijo Peter, puedes encapsular toda la seguridad del hilo dentro de la clase. Deberá tener cuidado con los eventos que expone o agrega, asegurándose de que se invoquen fuera de los lockings.

 public class SafeDictionary: IDictionary { private readonly object syncRoot = new object(); private Dictionary d = new Dictionary(); public void Add(TKey key, TValue value) { lock (syncRoot) { d.Add(key, value); } OnItemAdded(EventArgs.Empty); } public event EventHandler ItemAdded; protected virtual void OnItemAdded(EventArgs e) { EventHandler handler = ItemAdded; if (handler != null) handler(this, e); } // more IDictionary members... } 

Editar: los documentos de MSDN señalan que la enumeración no es inherentemente segura para subprocesos. Esa puede ser una razón para exponer un objeto de sincronización fuera de su clase. Otra forma de abordar eso sería proporcionar algunos métodos para realizar una acción en todos los miembros y bloquear la enumeración de los miembros. El problema con esto es que no sabe si la acción transferida a esa función llama a algún miembro de su diccionario (lo que daría lugar a un interlocking). La exposición del objeto de sincronización le permite al consumidor tomar esas decisiones y no oculta el punto muerto dentro de su clase.

La clase .NET 4.0 que admite concurrencia se denomina ConcurrentDictionary .

Intentar sincronizar internamente será casi seguro insuficiente porque tiene un nivel de abstracción demasiado bajo. Supongamos que realiza las operaciones Add y ContainsKey individualmente con subprocesos seguros de la siguiente manera:

 public void Add(TKey key, TValue value) { lock (this.syncRoot) { this.innerDictionary.Add(key, value); } } public bool ContainsKey(TKey key) { lock (this.syncRoot) { return this.innerDictionary.ContainsKey(key); } } 

Entonces, ¿qué sucede cuando llamas a este código de hilo supuestamente seguro para subprocesos? ¿Funcionará siempre bien?

 if (!mySafeDictionary.ContainsKey(someKey)) { mySafeDictionary.Add(someKey, someValue); } 

La respuesta simple es no. En algún punto, el método Add emitirá una excepción que indica que la clave ya existe en el diccionario. ¿Cómo puede ser esto con un diccionario seguro para subprocesos, podría preguntar? Bueno, solo porque cada operación es segura para subprocesos, la combinación de dos operaciones no lo es, ya que otro subproceso podría modificarla entre su llamada a ContainsKey y Add .

Lo que significa escribir este tipo de escenario correctamente, necesita un locking fuera del diccionario, por ejemplo

 lock (mySafeDictionary) { if (!mySafeDictionary.ContainsKey(someKey)) { mySafeDictionary.Add(someKey, someValue); } } 

Pero ahora, dado que tiene que escribir código de locking externo, está mezclando la sincronización interna y externa, lo que siempre genera problemas, como códigos poco claros y lockings. Por lo tanto, en última instancia, probablemente seas mejor para:

  1. Use un Dictionary normal Dictionary y sincronice externamente, encerrando las operaciones compuestas en él, o

  2. Escriba un nuevo contenedor seguro para subprocesos con una interfaz diferente (es decir, no IDictionary ) que combine las operaciones como un método AddIfNotContained para que nunca tenga que combinar operaciones desde él.

(Tiendo a ir con el # 1 yo mismo)

No debe publicar su objeto de locking privado a través de una propiedad. El objeto de locking debe existir en privado con el único propósito de actuar como un punto de encuentro.

Si el rendimiento demuestra ser pobre utilizando el locking estándar, la colección de cerraduras Power Threading de Wintellect puede ser muy útil.

Hay varios problemas con el método de implementación que está describiendo.

  1. Nunca deberías exponer tu objeto de sincronización. Si lo hace, se abrirá a un consumidor que agarra el objeto y lo prende y luego está tostado.
  2. Está implementando una interfaz no segura para subprocesos con una clase segura para subprocesos. En mi humilde opinión esto le costará en el camino

Personalmente, he encontrado que la mejor forma de implementar una clase de seguridad de subprocesos es a través de la inmutabilidad. Realmente reduce la cantidad de problemas que puede encontrar con la seguridad del hilo. Consulte el Blog de Eric Lippert para más detalles.

No necesita bloquear la propiedad SyncRoot en sus objetos de consumo. El locking que tiene dentro de los métodos del diccionario es suficiente.

Para elaborar: lo que termina sucediendo es que su diccionario está bloqueado por un período de tiempo más largo de lo necesario.

Lo que sucede en tu caso es el siguiente:

Say thread A adquiere el locking en SyncRoot antes de la llamada a m_mySharedDictionary.Add. El hilo B intenta adquirir el locking pero está bloqueado. De hecho, todos los otros hilos están bloqueados. El subproceso A puede llamar al método Agregar. En la instrucción de locking dentro del método Agregar, el hilo A puede obtener el locking nuevamente porque ya lo posee. Al salir del contexto de locking dentro del método y luego fuera del método, el hilo A ha liberado todos los lockings permitiendo que otros hilos continúen.

Simplemente puede permitir que cualquier consumidor llame al método Agregar ya que la instrucción de locking dentro de su método de adición de clase SharedDictionary tendrá el mismo efecto. En este momento, tiene un locking redundante. Solo bloquearía SyncRoot fuera de uno de los métodos del diccionario si tuviera que realizar dos operaciones en el objeto de diccionario que se debe garantizar que ocurra de forma consecutiva.

Solo un pensamiento, ¿por qué no recrear el diccionario? Si leer es una gran cantidad de escritura, el locking sincronizará todas las solicitudes.

ejemplo

  private static readonly object Lock = new object(); private static Dictionary _dict = new Dictionary(); private string Fetch(string key) { lock (Lock) { string returnValue; if (_dict.TryGetValue(key, out returnValue)) return returnValue; returnValue = "find the new value"; _dict = new Dictionary(_dict) { { key, returnValue } }; return returnValue; } } public string GetValue(key) { string returnValue; return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key); } 

Colecciones y sincronización