2008-10-01 20 views
105

que fue capaz de poner en práctica un diccionario flujos seguros en C# mediante la derivación de IDictionary y la definición de un objeto privado SyncRoot:¿Cuál es la mejor manera de implementar un diccionario seguro para subprocesos?

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue> 
{ 
    private readonly object syncRoot = new object(); 
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>(); 

    public object SyncRoot 
    { 
     get { return syncRoot; } 
    } 

    public void Add(TKey key, TValue value) 
    { 
     lock (syncRoot) 
     { 
      d.Add(key, value); 
     } 
    } 

    // more IDictionary members... 
} 

entonces me bloqueo en este objeto SyncRoot lo largo de mis consumidores (múltiples hilos):

Ejemplo:

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

yo era capaz de hacer que funcione, pero esto dio lugar a un cierto código feo. Mi pregunta es, ¿existe una forma mejor y más elegante de implementar un Diccionario seguro para subprocesos?

+2

qué es lo que encuentran desagradable al respecto? –

+1

Creo que se está refiriendo a todas las declaraciones de bloqueo que tiene a lo largo de su código dentro de los consumidores de la clase SharedDictionary: está bloqueando el código * de llamada cada vez que accede a un método en un objeto SharedDictionary. –

+0

En lugar de usar el método Add, intente realizar asignando valores exm_MySharedDictionary ["key1"] = "item1", esto es seguro para subprocesos. – testuser

Respuesta

43

Como dijo Peter, puede 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 bloqueos.

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue> 
{ 
    private readonly object syncRoot = new object(); 
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>(); 

    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 docs MSDN señalan que enumeración está inherentemente no hilo de seguridad. 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 interbloqueo). 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.

+0

@fryguybob: Enumeración fue en realidad la única razón por la que estaba exponiendo el objeto de sincronización. Por convención, realizaría un bloqueo en ese objeto solo cuando estoy enumerando a través de la colección. –

+1

Si su diccionario no es demasiado grande, puede enumerar en una copia y tenerlo incorporado en la clase. – fryguybob

+1

Mi diccionario no es demasiado grande, y creo que fue el truco. Lo que hice fue crear un nuevo método público llamado CopyForEnum() que devuelve una nueva instancia de un diccionario con copias del diccionario privado. Este método fue llamado para enumarations, y SyncRoot fue eliminado. ¡Gracias! –

2

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

Para elaborar: Lo que sucede es que su diccionario está bloqueado por un período de tiempo más largo que el necesario.

Lo que sucede en su caso es el siguiente:

Say hilo A adquiere el bloqueo en SyncRoot antes de la llamada a m_mySharedDictionary.Add. El hilo B intenta adquirir el bloqueo 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 bloqueo dentro del método Agregar, el hilo A puede obtener el bloqueo nuevamente porque ya lo posee. Al salir del contexto de bloqueo dentro del método y luego fuera del método, el hilo A ha liberado todos los bloqueos permitiendo que otros hilos continúen.

Simplemente puede permitir que cualquier consumidor llame al método Agregar, ya que la instrucción de bloqueo dentro de su clase SharedDictionary Agregar método tendrá el mismo efecto. En este momento, tiene un bloqueo 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.

+1

No es cierto ... si realiza dos operaciones después de otra que son internamente seguras para subprocesos, eso no significa que el bloque de código general sea seguro para subprocesos. Por ejemplo: if (! MyDict.ContainsKey (someKey)) { myDict.Add (someKey, someValue); } no sería seguro para los hilos, incluso ContainsKey y Add son threadsafe – Tim

+0

Su punto es correcto, pero está fuera de contexto con mi respuesta y la pregunta. Si miras la pregunta, no habla sobre llamar a ContainsKey, ni tampoco mi respuesta. Mi respuesta se refiere a la adquisición de un bloqueo en SyncRoot que se muestra en el ejemplo en la pregunta original. Dentro del contexto de la declaración de bloqueo, una o más operaciones de seguridad de subprocesos se ejecutarían de manera segura. –

+0

Supongo que todo lo que hace es agregar al diccionario, pero como tiene "// más miembros de IDictionary ...", supongo que en algún momento también querrá volver a leer los datos del diccionario. Si ese es el caso, entonces debe haber algún mecanismo de bloqueo accesible externamente. No importa si es SyncRoot en el diccionario u otro objeto utilizado únicamente para el bloqueo, pero sin dicho esquema, el código general no será seguro para subprocesos. – Tim

6

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

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

4

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

  1. Nunca debe exponer su 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 la mejor manera 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 Eric Lippert's Blog para obtener más detalles.

58

Intentar sincronizar internamente será casi seguro insuficiente porque tiene un nivel de abstracción demasiado bajo. Digamos que hacen las operaciones Add y ContainsKey individual apta para subprocesos 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é ocurre cuando se llama este bit supuestamente segura para los subprocesos de código de múltiples hilos? ¿Funcionará siempre bien?

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

La respuesta simple es no. En algún momento, 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 al ContainsKey y Add.

Lo que significa escribir correctamente este tipo de escenario, necesita un candado afuera del diccionario, p.

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

Pero ahora, ya que tienes que escribir el código de bloqueo de forma externa, que va a mezclar la sincronización interna y externa, que siempre conduce a problemas tales como código claro y callejones sin salida. Así que en última instancia, usted es probablemente mejor que cualquiera de los dos:

  1. uso normal Dictionary<TKey, TValue> y sincronizar el exterior, encerrando las operaciones compuestas en él, o

  2. escribir un nuevo envoltorio seguro para subprocesos con una interfaz diferente (es decir, no IDictionary<T>) que combina las operaciones como un método AddIfNotContained, por lo que nunca necesitará combinar operaciones desde él.

(que tienden a ir con # 1 yo)

+10

Vale la pena señalar que .NET 4.0 incluirá un montón de contenedores seguros para subprocesos tales como colecciones y diccionarios, que tienen una interfaz diferente a la colección estándar (es decir, están haciendo la opción 2 anterior para usted). –

+1

También vale la pena señalar que la granularidad ofrecida por el bloqueo incluso cruda a menudo será suficiente para un enfoque de varios lectores de un solo escritor si se diseña un enumerador adecuado que le permita a la clase subyacente saber cuándo se desecha (métodos que querrían escribir el diccionario mientras que un enumerador no expuesto debe reemplazar el diccionario con una copia). – supercat

199

La clase .NET 4.0 que es compatible con la concurrencia se llama ConcurrentDictionary.

+10

+1. Esa es la respuesta correcta. –

+4

Marque esto como respuesta, no necesita dictámenes personalizados si el .Net tiene una solución –

+25

(Recuerde que la otra respuesta se escribió mucho antes de la existencia de .NET 4.0 (lanzado en 2010).) –

0

¿Por qué no recrear el diccionario? Si leer es una gran cantidad de escritura, el bloqueo sincronizará todas las solicitudes.

ejemplo

private static readonly object Lock = new object(); 
    private static Dictionary<string, string> _dict = new Dictionary<string, string>(); 

    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<string, string>(_dict) { { key, returnValue } }; 

      return returnValue; 
     } 
    } 

    public string GetValue(key) 
    { 
     string returnValue; 

     return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key); 
    } 
Cuestiones relacionadas