'usar' declaración vs 'intentar finalmente'


81

Tengo un montón de propiedades en las que voy a usar bloqueos de lectura / escritura. Puedo implementarlos con una try finallyo una usingcláusula.

En el try finallyI adquiriría el bloqueo antes del try, y lo liberaría en el finally. En la usingcláusula, crearía una clase que adquiere el bloqueo en su constructor y lo libera en su método Dispose.

Estoy usando bloqueos de lectura / escritura en muchos lugares, así que he estado buscando formas que puedan ser más concisas que try finally. Estoy interesado en escuchar algunas ideas sobre por qué una forma puede no ser recomendada o por qué una puede ser mejor que otra.

Método 1 ( try finally):

static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
private DateTime dtMyDateTime_m
public DateTime MyDateTime
{
    get
    {
        rwlMyLock_m .AcquireReaderLock(0);
        try
        {
            return dtMyDateTime_m
        }
        finally
        {
            rwlMyLock_m .ReleaseReaderLock();
        }
    }
    set
    {
        rwlMyLock_m .AcquireWriterLock(0);
        try
        {
            dtMyDateTime_m = value;
        }
        finally
        {
            rwlMyLock_m .ReleaseWriterLock();
        }
    }
}

Método 2:

static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
private DateTime dtMyDateTime_m
public DateTime MyDateTime
{
    get
    {
        using (new ReadLock(rwlMyLock_m))
        {
            return dtMyDateTime_m;
        }
    }
    set
    {
        using (new WriteLock(rwlMyLock_m))
        {
            dtMyDateTime_m = value;
        }
    }
}

public class ReadLock : IDisposable
{
    private ReaderWriterLock rwl;
    public ReadLock(ReaderWriterLock rwl)
    {
        this.rwl = rwl;
        rwl.AcquireReaderLock(0);
    }

    public void Dispose()
    {
        rwl.ReleaseReaderLock();
    }
}

public class WriteLock : IDisposable
{
    private ReaderWriterLock rwl;
    public WriteLock(ReaderWriterLock rwl)
    {
        this.rwl = rwl;
        rwl.AcquireWriterLock(0);
    }

    public void Dispose()
    {
        rwl.ReleaseWriterLock();
    }
}

1
Como ya se indicó en muchas respuestas, el Método 2 es muy bueno, pero para evitar la basura en el montón cada vez que usa el bloqueo, debe cambiar ReadLock y WriteLock a structs. Aunque la instrucción using usa la interfaz IDisposable de una estructura, ¡C # es lo suficientemente inteligente como para evitar el boxeo!
Michael Gehling

Respuestas:


94

Desde MSDN, usando Statement (Referencia de C #)

La instrucción using garantiza que se llame a Dispose incluso si se produce una excepción mientras llama a métodos en el objeto. Puede lograr el mismo resultado colocando el objeto dentro de un bloque try y luego llamando a Dispose en un bloque finalmente; de hecho, así es como el compilador traduce la declaración using. El ejemplo de código anterior se expande al siguiente código en tiempo de compilación (tenga en cuenta las llaves adicionales para crear el alcance limitado para el objeto):

{
  Font font1 = new Font("Arial", 10.0f);
  try
  {
    byte charset = font1.GdiCharSet;
  }
  finally
  {
    if (font1 != null)
      ((IDisposable)font1).Dispose();
  }
}

Básicamente, es el mismo código pero con una buena verificación automática de nulos y un alcance adicional para su variable . La documentación también establece que "asegura el uso correcto del objeto IDisposable", por lo que también podría obtener un soporte de marco aún mejor para cualquier caso oscuro en el futuro.

Así que ve con la opción 2.

Tener la variable dentro de un alcance que finaliza inmediatamente después de que ya no se necesita también es una ventaja.


¿Qué es mejor para liberar un recurso que no se puede instanciar dentro de una declaración de uso o que se reutiliza o se pasa como un parámetro? try / catch !!!
bjan

2
@bjan bueno, ¿por qué lo estás considerando usingen ese caso? eso no usinges para lo que es.
chakrit

1
es por eso que también menciono try/catchque parece la única forma de manejar a través del try/catch/finallybloque. esperaba usingpoder manejar esto también
bjan

Bueno, try/finallysupongo que es tu única opción en ese caso. En mi opinión, sin embargo, creo que siempre debería haber algún objeto / pieza de código responsable de mantener la vida útil del objeto en tales casos (donde siempre se debería llamar a Dispose ()). Si una clase solo maneja la instanciación y alguien más tiene que recordar para tirarlo creo que huele un poco allí. No estoy seguro de cómo agregaría eso a nivel de idioma.
chakrit

Creo que la única razón para evitar el "uso" es evitar el desechable por completo, ya que supongo que es otra instanciación de objeto.
Demetris Leptos

12

Definitivamente prefiero el segundo método. Es más conciso en el punto de uso y menos propenso a errores.

En el primer caso, alguien que edite el código debe tener cuidado de no insertar nada entre la llamada de bloqueo de adquisición (lectura | escritura) y el intento.

(Sin embargo, usar un bloqueo de lectura / escritura en propiedades individuales como esta suele ser excesivo. Es mejor aplicarlos a un nivel mucho más alto. Un bloqueo simple a menudo será suficiente aquí, ya que la posibilidad de disputa es presumiblemente muy pequeña dado el tiempo que se mantiene el bloqueo. para, y adquirir un bloqueo de lectura / escritura es una operación más costosa que un simple bloqueo).


¿Qué tal estar a prueba de fallas? Sé que un intento finalmente siempre activará el bloque final, ¿hay alguna forma de que no se llame a dispose?
Jeremy

1
No, la declaración using es esencialmente azúcar sintáctica para el patrón try /.
Blair Conrad

El modelo de uso asegura que el objeto siempre se eliminará.
Nathen Silver

Si usa reflector, verá que el bloque using es traducido por el compilador en una construcción try ... finalmente, por lo que a nivel IL son equivalentes. El 'uso' es simplemente azúcar sintético.
Rob Walker

^^ recuerde que hay escenarios potenciales donde finalmente no se llama. Un corte de energía, por ejemplo. ;)
Quisquilloso

8

Considere la posibilidad de que ambas soluciones sean malas porque enmascaran excepciones .

Un trysin un catchobviamente debería ser una mala idea; consulte MSDN para saber por qué la usingdeclaración es igualmente peligrosa.

Tenga en cuenta también que Microsoft ahora recomienda ReaderWriterLockSlim en lugar de ReaderWriterLock.

Por último, tenga en cuenta que los ejemplos de Microsoft utilizan dos bloques try-catch para evitar estos problemas, p. Ej.

try
{
    try
    {
         //Reader-writer lock stuff
    }
    finally
    {
         //Release lock
    }
 }
 catch(Exception ex)
 {
    //Do something with exception
 }

Una solución simple, consistente y limpia es un buen objetivo, pero asumiendo que no puede simplemente usar lock(this){return mydateetc;}, podría reconsiderar el enfoque; con más información, estoy seguro de que Stack Overflow puede ayudar ;-)


2
Un intento finalmente no enmascara necesariamente la excepción. En mi ejemplo, se adquiere el bloqueo, luego, si se lanza alguna excepción dentro del alcance, el bloqueo se liberará, pero la excepción seguirá apareciendo.
Jeremy

1
@Jeremy: si su bloque finalmente arroja una excepción, enmascarará la excepción lanzada en su bloque de prueba; eso es lo que decía el artículo de msdn que era el (mismo) problema con la sintaxis de uso
Steven A. Lowe

3
No enmascarará la excepción, la reemplazará exactamente de la misma manera que la suya.
Jon Hanna

5

Yo personalmente uso la instrucción "using" de C # con tanta frecuencia como sea posible, pero hay algunas cosas específicas que hago junto con ella para evitar los problemas potenciales mencionados. Para ilustrar:

void doSomething()
{
    using (CustomResource aResource = new CustomResource())
    {
        using (CustomThingy aThingy = new CustomThingy(aResource))
        {
            doSomething(aThingy);
        }
    }
}

void doSomething(CustomThingy theThingy)
{
    try
    {
        // play with theThingy, which might result in exceptions
    }
    catch (SomeException aException)
    {
        // resolve aException somehow
    }
}

Tenga en cuenta que separo la instrucción "using" en un método y el uso del objeto (s) en otro método con un bloque "try" / "catch". Puedo anidar varias declaraciones de "uso" como esta para objetos relacionados (a veces profundizo tres o cuatro en mi código de producción).

En mis Dispose()métodos para estas IDisposableclases personalizadas , capturo excepciones (pero NO errores) y las registro (usando Log4net). Nunca me he encontrado con una situación en la que alguna de esas excepciones pueda afectar mi procesamiento. Se permite que los errores potenciales, como de costumbre, se propaguen por la pila de llamadas y, por lo general, finalicen el procesamiento con un mensaje apropiado (el error y el seguimiento de la pila) registrado.

Si de alguna manera me encontrara con una situación en la que pudiera ocurrir una excepción significativa durante Dispose(), lo rediseñaría para esa situación. Francamente, dudo que eso suceda alguna vez.

Mientras tanto, el alcance y las ventajas de limpieza de "usar" lo convierten en una de mis funciones favoritas de C #. Por cierto, trabajo en Java, C # y Python como mis lenguajes principales, con muchos otros lanzados aquí y allá, y "usar" es una de mis características de lenguaje favoritas porque es un caballo de batalla práctico y diario. .


Sugerencia, para la legibilidad del código: alinee y comprima las declaraciones de usos sin tener llaves excepto para el "uso" interno.
Bent Tranberg

4

Me gusta la tercera opcion

private object _myDateTimeLock = new object();
private DateTime _myDateTime;

public DateTime MyDateTime{
  get{
    lock(_myDateTimeLock){return _myDateTime;}
  }
  set{
    lock(_myDateTimeLock){_myDateTime = value;}
  }
}

De sus dos opciones, la segunda opción es la más limpia y fácil de entender lo que está sucediendo.


La instrucción de bloqueo no funciona de la misma manera que ReaderWriterLock.
chakrit

@chakrit: No, pero a menos que sepa que en realidad hay una disputa de bloqueo, es probable que sean más eficaces.
Michael Burr

Es cierto, pero siempre debes optar por el bloqueo smiple primero. La pregunta del subby no indica nada sobre problemas de rendimiento o requisitos que no permitan un bloqueo. Por lo tanto, no hay razón para intentar ser hábil. Solo ciérralo y muévelo.

4

"Montón de propiedades" y el bloqueo en el nivel de captador y definidor de propiedades parece incorrecto. Su bloqueo es demasiado fino. En el uso de objetos más típico, querrá asegurarse de haber adquirido un candado para acceder a más de una propiedad al mismo tiempo. Su caso específico puede ser diferente, pero lo dudo un poco.

De todos modos, adquirir el candado cuando acceda al objeto en lugar de a la propiedad reducirá significativamente la cantidad de código de bloqueo que tendrá que escribir.


Sí, definitivamente veo su punto. La mayoría de mis propiedades son en realidad bools, ints, que no necesitaría bloquear porque deberían ser atómicas. Hay algunos de fecha y hora, de cadena en los que me gustaría bloquearlos. debido a que la minoría necesitaría candados, es mejor que lo coloque en la propiedad
Jeremy

4

SECO dice: segunda solución. La primera solución duplica la lógica del uso de un candado, mientras que la segunda no.


1

Los bloques Try / Catch son generalmente para el manejo de excepciones, mientras que los bloques de uso se utilizan para garantizar que el objeto se elimine.

Para el bloqueo de lectura / escritura, un try / catch puede ser lo más útil, pero también puede usar ambos, así:

using (obj)
{
  try { }
  catch { }
}

para que pueda llamar implícitamente a su interfaz IDisposable, así como hacer conciso el manejo de excepciones.


0

Creo que el método 2 sería mejor.

  • Código más simple y legible en sus propiedades.
  • Menos propenso a errores, ya que no es necesario reescribir el código de bloqueo varias veces.

0

Si bien estoy de acuerdo con muchos de los comentarios anteriores, incluida la granularidad del bloqueo y el manejo de excepciones cuestionables, la cuestión es de enfoque. Permítanme darles una gran razón por la que prefiero usar el modelo try {} finalmente ... abstracción.

Tengo un modelo muy similar al tuyo con una excepción. Definí una interfaz base ILock y en ella proporcioné un método llamado Acquire (). El método Acquire () devolvió el objeto IDisposable y, como resultado, significa que siempre que el objeto con el que estoy tratando sea de tipo ILock, se puede usar para hacer un alcance de bloqueo. ¿Porque es esto importante?

Nos ocupamos de muchos mecanismos y comportamientos de bloqueo diferentes. Su objeto de bloqueo puede tener un tiempo de espera específico que emplea. Su implementación de bloqueo puede ser un bloqueo de monitor, bloqueo de lector, bloqueo de escritura o bloqueo de giro. Sin embargo, desde la perspectiva de la persona que llama, todo eso es irrelevante, lo que les importa es que el contrato para bloquear el recurso se cumpla y que el bloqueo lo haga de manera coherente con su implementación.

interface ILock {
    IDisposable Acquire();
}

class MonitorLock : ILock {
    IDisposable Acquire() { ... acquire the lock for real ... }
}

Me gusta tu modelo, pero consideraría ocultar la mecánica de la cerradura a la persona que llama. FWIW, he medido la sobrecarga de la técnica de uso frente a la de intentar finalmente y la sobrecarga de asignar el objeto desechable tendrá entre un 2-3% de sobrecarga de rendimiento.


0

Me sorprende que nadie haya sugerido encapsular el intento finalmente en funciones anónimas. Al igual que la técnica de instanciar y eliminar clases con la instrucción using, esto mantiene el bloqueo en un solo lugar. Yo prefiero esto solo porque prefiero leer la palabra "finalmente" que la palabra "Eliminar" cuando estoy pensando en abrir un candado.

class StackOTest
{
    private delegate DateTime ReadLockMethod();
    private delegate void WriteLockMethod();

    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            return ReadLockedMethod(
                rwlMyLock_m,
                delegate () { return dtMyDateTime_m; }
            );
        }
        set
        {
            WriteLockedMethod(
                rwlMyLock_m,
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private static DateTime ReadLockedMethod(
        ReaderWriterLock rwl,
        ReadLockMethod method
    )
    {
        rwl.AcquireReaderLock(0);
        try
        {
            return method();
        }
        finally
        {
            rwl.ReleaseReaderLock();
        }
    }

    private static void WriteLockedMethod(
        ReaderWriterLock rwl,
        WriteLockMethod method
    )
    {
        rwl.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwl.ReleaseWriterLock();
        }
    }
}

0

SoftwareJedi, no tengo una cuenta, así que no puedo editar mis respuestas.

En cualquier caso, la versión anterior no era realmente buena para uso general, ya que el bloqueo de lectura siempre requería un valor de retorno. Esto corrige que:

class StackOTest
{
    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            DateTime retval = default(DateTime);
            ReadLockedMethod(
                delegate () { retval = dtMyDateTime_m; }
            );
            return retval;
        }
        set
        {
            WriteLockedMethod(
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private void ReadLockedMethod(Action method)
    {
        rwlMyLock_m.AcquireReaderLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseReaderLock();
        }
    }

    private void WriteLockedMethod(Action method)
    {
        rwlMyLock_m.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseWriterLock();
        }
    }
}

0

Lo siguiente crea métodos de extensión para la clase ReaderWriterLockSlim que le permiten hacer lo siguiente:

var rwlock = new ReaderWriterLockSlim();
using (var l = rwlock.ReadLock())
{
     // read data
}
using (var l = rwlock.WriteLock())
{
    // write data
}

Aquí está el código:

static class ReaderWriterLockExtensions() {
    /// <summary>
    /// Allows you to enter and exit a read lock with a using statement
    /// </summary>
    /// <param name="readerWriterLockSlim">The lock</param>
    /// <returns>A new object that will ExitReadLock on dispose</returns>
    public static OnDispose ReadLock(this ReaderWriterLockSlim readerWriterLockSlim)
    {
        // Enter the read lock
        readerWriterLockSlim.EnterReadLock();
        // Setup the ExitReadLock to be called at the end of the using block
        return new OnDispose(() => readerWriterLockSlim.ExitReadLock());
    }
    /// <summary>
    /// Allows you to enter and exit a write lock with a using statement
    /// </summary>
    /// <param name="readerWriterLockSlim">The lock</param>
    /// <returns>A new object that will ExitWriteLock on dispose</returns>
    public static OnDispose WriteLock(this ReaderWriterLockSlim rwlock)
    {
        // Enter the write lock
        rwlock.EnterWriteLock();
        // Setup the ExitWriteLock to be called at the end of the using block
        return new OnDispose(() => rwlock.ExitWriteLock());
    }
}

/// <summary>
/// Calls the finished action on dispose.  For use with a using statement.
/// </summary>
public class OnDispose : IDisposable
{
    Action _finished;

    public OnDispose(Action finished) 
    {
        _finished = finished;
    }

    public void Dispose()
    {
        _finished();
    }
}

0

En realidad, en su primer ejemplo, para que las soluciones sean comparables, también las implementaría IDisposableallí. Entonces llamarías Dispose()desde el finallybloque en lugar de liberar el bloqueo directamente.

Entonces sería una implementación de "manzanas con manzanas" (y MSIL) en sentido (MSIL será el mismo para ambas soluciones). Probablemente sea una buena idea usar usingdebido al alcance agregado y porque el Framework garantizará el uso adecuado de IDisposable(este último es menos beneficioso si lo está implementando IDisposableusted mismo).


-1

Tonto de mí. Hay una manera de hacerlo aún más simple haciendo que los métodos bloqueados formen parte de cada instancia (en lugar de estáticos como en mi publicación anterior). Ahora realmente prefiero esto porque no hay necesidad de pasar `rwlMyLock_m 'a otra clase o método.

class StackOTest
{
    private delegate DateTime ReadLockMethod();
    private delegate void WriteLockMethod();

    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            return ReadLockedMethod(
                delegate () { return dtMyDateTime_m; }
            );
        }
        set
        {
            WriteLockedMethod(
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private DateTime ReadLockedMethod(ReadLockMethod method)
    {
        rwlMyLock_m.AcquireReaderLock(0);
        try
        {
            return method();
        }
        finally
        {
            rwlMyLock_m.ReleaseReaderLock();
        }
    }

    private void WriteLockedMethod(WriteLockMethod method)
    {
        rwlMyLock_m.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseWriterLock();
        }
    }
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.