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

109

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

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...
}

Luego bloqueo este objeto SyncRoot en todos mis consumidores (múltiples subprocesos):

Ejemplo:

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

Pude hacerlo funcionar, pero esto resultó en un código desagradable. Mi pregunta es, ¿existe una forma mejor y más elegante de implementar un diccionario seguro para subprocesos?

GP.
fuente
3
¿Qué es lo que le parece feo?
Asaf R
1
Creo que se refiere 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.
Peter Meyer
En lugar de usar el método Add, intente hacerlo asignando valores ex- m_MySharedDictionary ["key1"] = "item1", esto es seguro para subprocesos.
testuser

Respuestas:

43

Como dijo Peter, puede encapsular toda la seguridad de los subprocesos dentro de la clase. Deberá tener cuidado con cualquier evento que exponga o agregue, 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 documentos de MSDN señalan que la enumeración no es inherentemente segura para subprocesos. Esa puede ser una de las razones 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 pasada a esa función llama a algún miembro de su diccionario (eso resultaría en un punto muerto). Exponer el objeto de sincronización permite al consumidor tomar esas decisiones y no oculta el punto muerto dentro de su clase.

fryguybob
fuente
@fryguybob: La 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.
GP.
1
Si su diccionario no es demasiado grande, puede enumerarlo en una copia y tenerlo integrado en la clase.
fryguybob
2
Mi diccionario no es demasiado grande y creo que eso funcionó. 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. A continuación, se llamó a este método para enumeraciones y se eliminó SyncRoot. ¡Gracias!
GP.
12
Esta tampoco es una clase inherentemente segura para subprocesos, ya que las operaciones de diccionario tienden a ser granulares. Un poco de lógica en la línea de if (dict.Contains (lo que sea)) {dict.Remove (lo que sea); dict.Add (lo que sea, newval); } es sin duda una condición de carrera a la espera de suceder.
zócalo
207

La clase .NET 4.0 que admite la simultaneidad se denomina ConcurrentDictionary.

Hector correa
fuente
4
Marque esto como respuesta, no necesita un diccionario personalizado si el propio .Net tiene una solución
Alberto León
27
(Recuerde que la otra respuesta se escribió mucho antes de la existencia de .NET 4.0 (lanzado en 2010).)
Jeppe Stig Nielsen
1
Desafortunadamente, no es una solución libre de bloqueos, por lo que es inútil en ensamblajes seguros CLR de SQL Server. Necesitaría algo como lo que se describe aquí: cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdf o quizás esta implementación: github.com/hackcraft/Ariadne
Triynko
2
Realmente viejo, lo sé, pero es importante tener en cuenta que el uso de ConcurrentDictionary frente a un diccionario puede resultar en pérdidas de rendimiento significativas. Es muy probable que esto sea el resultado de un costoso cambio de contexto, así que asegúrese de que necesita un diccionario seguro para subprocesos antes de usar uno.
cruzado el
60

Es casi seguro que intentar sincronizar internamente será insuficiente porque tiene un nivel de abstracción demasiado bajo. Supongamos que hace que las operaciones Addy ContainsKeysean seguras para subprocesos individualmente 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 bit de código supuestamente seguro para subprocesos desde varios subprocesos? ¿Siempre funcionará bien?

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

La respuesta simple es no. En algún momento, el Addmétodo lanzará una excepción que indica que la clave ya existe en el diccionario. ¿Cómo puede ser esto con un diccionario seguro para subprocesos? 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 modificarlo entre su llamada a ContainsKeyy Add.

Lo que significa que para escribir este tipo de escenario correctamente, necesita un candado 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 bloqueo externo, está mezclando la sincronización interna y externa, lo que siempre conduce a problemas como código poco claro y puntos muertos. Entonces, en última instancia, probablemente sea mejor para:

  1. Use un normal Dictionary<TKey, TValue>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<T>) que combine las operaciones, como un AddIfNotContainedmétodo, para que nunca tenga que combinar operaciones desde él.

(Tiendo a ir con el # 1 yo mismo)

Greg Beech
fuente
10
Vale la pena señalar que .NET 4.0 incluirá un montón de contenedores seguros para subprocesos, 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 por usted).
Greg Beech
1
También vale la pena señalar que la granularidad que ofrece incluso el bloqueo burdo a menudo será suficiente para un enfoque de lectores múltiples de un solo escritor si se diseña un enumerador adecuado que permita a la clase subyacente saber cuándo se elimina (métodos que desearían escribir el diccionario mientras existe un enumerador no dispuesto debe reemplazar el diccionario con una copia).
supercat
6

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

Si el rendimiento resulta ser deficiente con el candado estándar, la colección de candados Power Threading de Wintellect puede ser muy útil.

Jonathan Webb
fuente
5

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

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

Personalmente, he descubierto que la mejor manera de implementar una clase segura para subprocesos es a través de la inmutabilidad. Realmente reduce la cantidad de problemas que puede encontrar con la seguridad de subprocesos. Consulte el blog de Eric Lippert para obtener más detalles.

JaredPar
fuente
3

No es necesario bloquear la propiedad SyncRoot en sus objetos de consumidor. El bloqueo que tienes 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 del necesario.

Lo que ocurre en tu caso es lo siguiente:

Digamos que el hilo A adquiere el bloqueo en SyncRoot antes de la llamada a m_mySharedDictionary.Add. Luego, el hilo B intenta adquirir el bloqueo, pero se bloquea. De hecho, todos los demás subprocesos están bloqueados. El subproceso A puede llamar al método Add. En la declaración de bloqueo dentro del método Add, el subproceso 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 Add, ya que la instrucción de bloqueo dentro de su método Add de la clase SharedDictionary tendrá el mismo efecto. En este momento, tiene un bloqueo redundante. Solo bloquearía SyncRoot fuera de uno de los métodos de diccionario si tuviera que realizar dos operaciones en el objeto de diccionario que debían garantizarse que ocurran consecutivamente.

Peter Meyer
fuente
1
No es cierto ... si realiza dos operaciones tras 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 subprocesos, incluso si ContainsKey y Add son seguros para subprocesos
Tim
Su punto es correcto, pero está fuera de contexto con mi respuesta y la pregunta. Si miras la pregunta, no habla de llamar a ContainsKey, ni tampoco mi respuesta. Mi respuesta se refiere a adquirir un bloqueo en SyncRoot que se muestra en el ejemplo de la pregunta original. Dentro del contexto de la declaración de bloqueo, una o más operaciones seguras para subprocesos se ejecutarían de forma segura.
Peter Meyer
Supongo que si todo lo que hace es agregar algo 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 desde el exterior. No importa si es SyncRoot en el diccionario mismo u otro objeto utilizado únicamente para bloquear, pero sin tal esquema, el código general no será seguro para subprocesos.
Tim
El mecanismo de bloqueo externo es el que muestra en su ejemplo en la pregunta: lock (m_MySharedDictionary.SyncRoot) {m_MySharedDictionary.Add (...); } - sería perfectamente seguro hacer lo siguiente: lock (m_MySharedDictionary.SyncRoot) {if (! m_MySharedDictionary.Contains (...)) {m_MySharedDictionary.Add (...); }} En otras palabras, el mecanismo de bloqueo externo es la declaración de bloqueo que opera en la propiedad pública SyncRoot.
Peter Meyer
0

Solo un pensamiento, ¿por qué no recrear el diccionario? Si la lectura es una multitud 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);
    }
verbedr
fuente