2011-03-14 11 views

Respuesta

1

Brian Lambert escribió una publicación de blog titulada A simple and totally thread-safe implementation of IDisposable.
, incluye la siguiente aplicación:

using System; 
using System.Threading; 

/// <summary> 
/// DisposableBase class. Represents an implementation of the IDisposable interface. 
/// </summary> 
public abstract class DisposableBase : IDisposable 
{ 
    /// <summary> 
    /// A value which indicates the disposable state. 0 indicates undisposed, 1 indicates disposing 
    /// or disposed. 
    /// </summary> 
    private int disposableState; 

    /// <summary> 
    /// Finalizes an instance of the DisposableBase class. 
    /// </summary> 
    ~DisposableBase() 
    { 
     // The destructor has been called as a result of finalization, indicating that the object 
     // was not disposed of using the Dispose() method. In this case, call the DisposeResources 
     // method with the disposeManagedResources flag set to false, indicating that derived classes 
     // may only release unmanaged resources. 
     this.DisposeResources(false); 
    } 

    /// <summary> 
    /// Gets a value indicating whether the object is undisposed. 
    /// </summary> 
    public bool IsUndisposed 
    { 
     get 
     { 
      return Thread.VolatileRead(ref this.disposableState) == 0; 
     } 
    } 

    #region IDisposable Members 

    /// <summary> 
    /// Performs application-defined tasks associated with disposing of resources. 
    /// </summary> 
    public void Dispose() 
    { 
     // Attempt to move the disposable state from 0 to 1. If successful, we can be assured that 
     // this thread is the first thread to do so, and can safely dispose of the object. 
     if (Interlocked.CompareExchange(ref this.disposableState, 1, 0) == 0) 
     { 
      // Call the DisposeResources method with the disposeManagedResources flag set to true, indicating 
      // that derived classes may release unmanaged resources and dispose of managed resources. 
      this.DisposeResources(true); 

      // Suppress finalization of this object (remove it from the finalization queue and 
      // prevent the destructor from being called). 
      GC.SuppressFinalize(this); 
     } 
    } 

    #endregion IDisposable Members 

    /// <summary> 
    /// Dispose resources. Override this method in derived classes. Unmanaged resources should always be released 
    /// when this method is called. Managed resources may only be disposed of if disposeManagedResources is true. 
    /// </summary> 
    /// <param name="disposeManagedResources">A value which indicates whether managed resources may be disposed of.</param> 
    protected abstract void DisposeResources(bool disposeManagedResources); 
} 

Sin embargo, la totalidad absoluta y completa se disputa un poco en los comentarios, tanto en el blog y aquí.

+3

@Hans: No es fundamentalmente incorrecto; Es muy posible que múltiples hilos deseen negociar entre ellos, que lleguen a disponer del objeto cuando hayan terminado con él al mismo tiempo. ** Ese protocolo de negociación debe estar hecho de partes seguras. ** El blog de Brian no es "tonto". –

+1

Esto no es realmente seguro para subprocesos, ya que no aborda correctamente cómo debe comportarse el otro método llama en la clase.Si realmente quiere tener seguridad de hilos en Dispose, la forma más fácil es envolver todo el método Dispose en un candado y usar el mismo candado alrededor de cada método público y lanzar ObjectDisposedException si ya está dispuesto. Si necesita permitir que los métodos se ejecuten en paralelo además del método Dispose, puede usar un patrón MultiRead/SingleWrite, con Dispose como la operación 'Write'. –

+0

@Eric: si esta negociación se implementa correctamente, entonces no requiere que la implementación IDisposable sea segura para subprocesos. La negociación debe ser. No afirmé que el blog de Brian es chistoso por cierto. Perdón por ofenderte. –

29

No debe deshacerse de un objeto hasta que haya terminado con él. Si hay otros hilos que hacen referencia al objeto y existe la posibilidad de que quieran llamar a sus métodos, no debería deshacerse de él.

Por lo tanto, no es necesario que el Dispose sea seguro para subprocesos.

+2

En algunos casos, la forma preferida (si no es la única) para forzar el abandono de una operación de bloqueo de E/S es 'Eliminar 'la conexión desde abajo; dicha eliminación solo * puede * ocurrir desde un hilo externo (el el hilo bloqueado no puede hacer nada mientras está bloqueado). Es posible que un hilo externo decida "Eliminar" la conexión justo cuando la conexión está terminando su trabajo y el hilo que se ha bloqueado decide "Desechar" él mismo. – supercat

+0

Si hay un caso de uso para otro hilo forzando el abandono de una operación de bloqueo, eso requiere que alguien escriba el código para hacerlo funcionar. Sin duda, el implementador sería más prudente para implementar un método 'ForceAbandon' r que abuse de la semántica de 'Dispose'. Y si el implementador no se da cuenta de que este es un caso de uso requerido, tampoco puede esperar que 'Dispose' se comporte correctamente. Podría, pero eso sería solo suerte. – Ben

+1

He visto varias bibliotecas de E/S en las que la única operación de cruce de hilos que es compatible está matando una conexión. 'Dispose' parece la mejor opción. Además, en algunos casos, la forma más razonable de garantizar la limpieza de objetos parcialmente construidos puede involucrar objetos que se eliminan mutuamente, por lo que 'Object1.Dispose' podría llamar' Object2.Dispose', que a su vez podría llamar 'Object1.Dispose '. Protegerse de eso no requeriría 'Interbloqueado', pero aún requeriría un patrón mejor que el de Microsoft. – supercat

4

El único beneficio real para un patrón Dispose seguro de subprocesos es que se le puede garantizar que obtenga una ObjectDisposedException en lugar de un comportamiento potencialmente impredecible en caso de mal uso de hilos cruzados. Tenga en cuenta que esto significa que el patrón requiere más de un desecho seguro para hilos. requiere que todos los métodos que dependen de que la clase no esté Dispuesta se entrelacen adecuadamente con el mecanismo de Eliminación.

Es posible hacer esto, pero es un gran esfuerzo cubrir un caso límite que solo ocurre si hay un error de uso (es decir, un error).

+0

Sobre el esfuerzo involucrado: ¿Podríamos usar PostSharp para inyectar lógica de verificación en cada método público? La única razón por la que busco en la dirección de PostSharp es porque he visto otros idiomas hacer este tipo de cosas todo el tiempo. – GregC

+0

Si bien no hay muchas situaciones en las que varios hilos descubren simultáneamente que un objeto es inútil, hay muchas situaciones donde el último uso de un objeto puede ocurrir de forma asincrónica, o cuando el hecho de que un objeto sea elegible para recolección de basura implicará que otro objeto, al que todavía existe una referencia fuerte, se ha vuelto inútil. Un 'IDisposable 'garantizado en hilo sería útil en dichos contextos. – supercat

0

No estoy seguro de por qué Microsoft no utiliza un indicador de Disposición interbloqueado en el método de disposición no virtual (con la intención de que un finalizador, si lo hubiera, use el mismo indicador). Las situaciones en las que varios hilos pueden intentar deshacerse de un objeto son raras, pero no están prohibidas. Podría ocurrir, por ejemplo, con objetos que se supone que realizan alguna tarea asincrónica y limpian después de ellos mismos, pero que pueden eliminarse temprano si es necesario. La eliminación de objetos no debe ocurrir con la frecuencia suficiente para que un Interbloque.Exchange tenga un costo de rendimiento significativo.

Por otro lado, es importante tener en cuenta que, si bien proteger Dispose contra múltiples invocaciones es una política acertada en mi humilde opinión, no es suficiente hacer que Dispose realmente sea seguro para subprocesos. También es necesario asegurarse de que llamar a Dispose en un objeto que está en uso deje las cosas en buen estado. A veces, el mejor patrón para esto es disponer de un marcador "KillMeNow" y luego en un bloque protegido por Monitor.TryEnter, desechar el objeto. Cada rutina (que no sea Dispose) que utiliza el objeto debe adquirir el bloqueo durante la operación, pero pruebe ambos antes de adquirir y después de soltar el bloqueo para ver si KillMeNow está configurado; si es así, haga un Monitor.TryEnter y realice la lógica de eliminación.

Un problema mayor al hacer que un hilo seguro es identificable es el hecho de que Microsoft no especifica que el método RemoveHandler de un evento debe ser seguro para subprocesos sin riesgo de interbloqueo. Con frecuencia es necesario para un IDisposable. Desea eliminar los controladores de eventos; sin una forma segura de hacer thread-safe, es casi imposible escribir un Dispose seguro para subprocesos.

Cuestiones relacionadas