c# - ejemplos - awk unix
''using'' statement vs ''try finally'' (15)
Tengo un montón de propiedades en las que voy a usar bloqueos de lectura / escritura. Puedo implementarlos ya sea con una try finally
o una cláusula using
.
try finally
en el try finally
, adquiriría la cerradura antes del try
, y la liberaría finally
. En la cláusula using
, crearía una clase que adquiera el bloqueo en su constructor y lo libere en su método Dispose.
Estoy usando bloqueos de lectura / escritura en muchos lugares, así que he estado buscando formas que podrían ser más concisas que try finally
. Estoy interesado en escuchar algunas ideas sobre por qué no se puede recomendar una manera, o por qué una podría 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();
}
}
"Montón de propiedades" y el bloqueo en el captador de propiedades y el nivel del instalador se ve mal. Su bloqueo es demasiado fino. En la mayoría de los usos típicos de objetos, querrá asegurarse de haber adquirido un bloqueo para acceder a más de una propiedad al mismo tiempo. Su caso específico podría ser diferente, pero lo dudo.
De todos modos, adquirir el candado cuando accede al objeto en lugar de la propiedad reducirá significativamente la cantidad de código de bloqueo que tendrá que escribir.
A continuación, se crean 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();
}
}
Considere la posibilidad de que ambas soluciones sean malas porque ocultan excepciones .
Un try
sin catch
obviamente debería ser una mala idea; ver MSDN para saber por qué la declaración de using
es igualmente peligrosa.
Tenga en cuenta también que Microsoft ahora recomienda ReaderWriterLockSlim lugar de ReaderWriterLock.
Finalmente, tenga en cuenta que los ejemplos de Microsoft utilizan dos bloques try-catch para evitar estos problemas, por ejemplo
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 suponiendo que no puede usar lock(this){return mydateetc;}
, puede reconsiderar el enfoque; con más información, estoy seguro de que puede ayudar ;-)
Creo que el método 2 sería mejor.
- Código más simple y más legible en sus propiedades.
- Menos propenso a errores ya que el código de bloqueo no tiene que volver a escribirse varias veces.
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 edita el código debe tener cuidado de no insertar nada entre la llamada de bloqueo Adquirir (leer | Escribir) y el intento.
(El uso de un bloqueo de lectura / escritura en propiedades individuales como este suele ser exagerado. Se aplican mejor en un nivel mucho más alto. A menudo, bastará con un simple bloqueo, ya que la posibilidad de contención es presumiblemente muy pequeña dado el tiempo que se lleva a cabo el bloqueo para, y la adquisición de un bloqueo de lectura / escritura es una operación más costosa que un simple bloqueo).
Desde MSDN, usando Statement (C # Reference)
La instrucción using garantiza que se invoque Dispose incluso si se produce una excepción mientras se están llamando métodos al objeto. Puede lograr el mismo resultado colocando el objeto dentro de un bloque try y luego llamando a Dispose en un bloque finally; de hecho, así es como el compilador traduce la sentencia 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 ámbito que finaliza inmediatamente después de que ya no se necesita también es un plus.
En realidad, en su primer ejemplo, para hacer que las soluciones sean comparables, también implementaría "IDisposable" también allí, y llamaría "Dispose" del bloque "Finally" en lugar de liberar el bloqueo directamente. Entonces sería la implementación de "manzanas a manzanas" (y MSIL) -wise (MSIL será la misma para ambas soluciones). Todavía es una buena idea usar "usar" debido a la ampliación del alcance y porque el Framework asegurará el uso correcto de "IDisposable" (este último es menos beneficioso si está implementando "IDisposabe").
Los bloques Try / Catch generalmente son para el manejo de excepciones, mientras que el uso de bloques se usa para asegurar que el objeto sea eliminado.
Para el bloqueo de lectura / escritura, una prueba / captura podría ser la más útil, pero también podría usar ambas, como las siguientes:
using (obj)
{
try { }
catch { }
}
para que pueda llamar implícitamente su interfaz IDisposable y hacer que el manejo de excepciones sea conciso.
Me gusta la tercera opción
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 más fácil de entender lo que está sucediendo.
Me sorprende que nadie haya sugerido encapsular la prueba, finalmente, en funciones anónimas. Al igual que la técnica de creación de instancias y eliminación de clases con la instrucción using, esto mantiene el bloqueo en un solo lugar. Prefiero esto solo porque prefiero leer la palabra "finalmente" que la palabra "Eliminar" cuando estoy pensando en liberar 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();
}
}
}
Si bien estoy de acuerdo con muchos de los comentarios anteriores, incluida la granularidad del bloqueo y el cuestionable manejo de excepciones, la pregunta es de enfoque. Déjame darte una razón importante por la que prefiero utilizar el truco {} finalmente modelo ... abstracción.
Tengo una modelo muy similar a la tuya 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 del tipo ILock, se puede usar para establecer un ámbito 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. La implementación de su bloqueo puede ser un bloqueo de monitor, un bloqueo de lector, un bloqueo de escritor o un 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 la cerradura lo haga de manera consistente con su implementación.
interface ILock {
IDisposable Acquire();
}
class MonitorLock : ILock {
IDisposable Acquire() { ... acquire the lock for real ... }
}
Me gusta su modelo, pero consideraría ocultar la mecánica de bloqueo de la persona que llama. FWIW, he medido la sobrecarga de la técnica de uso versus la prueba final y la sobrecarga de asignar el objeto desechable tendrá entre un 2-3% de rendimiento por encima.
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 soluciona eso:
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();
}
}
}
Tonto de mí. Hay una manera de hacerlo aún más simple al hacer que los métodos bloqueados sean parte de cada instancia (en lugar de estar estáticos como en mi publicación anterior). Ahora realmente lo prefiero porque no hay necesidad de pasar `rwlMyLock_m ''a alguna 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();
}
}
}
Yo personalmente uso la declaración "usar" C # tan a menudo como sea posible, pero hay algunas cosas específicas que hago junto con esto para evitar los posibles problemas 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 separa 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 tengo tres o cuatro letras de profundidad en mi código de producción).
En mis métodos Dispose()
para estas clases personalizables IDisposable
, capturo excepciones (pero NO errores) y las registro (usando Log4net). Nunca me encontré con una situación en la que alguna de esas excepciones pudiera afectar mi procesamiento. Los errores potenciales, como de costumbre, pueden propagarse por la pila de llamadas y normalmente terminan el procesamiento con un mensaje apropiado (el error y el seguimiento de la pila) registrado.
Si de alguna manera encontré una situación en la que podría ocurrir una excepción significativa durante Dispose()
, 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 características favoritas de C #. Por cierto, trabajo en Java, C # y Python como mis lenguajes principales, con muchos otros arrojados aquí y allá, y "usar" es una de mis características de lenguaje favoritas porque es un caballo de batalla práctico y cotidiano. .
DRY dice: segunda solución. La primera solución duplica la lógica de usar un bloqueo, mientras que la segunda no.