‘using’ statement vs ‘try finally’

Tengo un montón de propiedades en las que voy a usar lockings 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 locking en su constructor y lo libere en su método Dispose.

Estoy usando lockings 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(); } } 

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 comstackdor traduce la sentencia using. El ejemplo de código anterior se expande al siguiente código en tiempo de comstackción (tenga en cuenta las llaves adicionales para crear el scope 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 scope 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.

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 locking Adquirir (leer | Escribir) y el bash.

(El uso de un locking 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 locking, ya que la posibilidad de contención es presumiblemente muy pequeña dado el tiempo que se lleva a cabo el locking para, y la adquisición de un locking de lectura / escritura es una operación más costosa que un simple locking).

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 statement de using es igualmente peligrosa.

Tenga en cuenta también que Microsoft ahora recomienda ReaderWriterLockSlim en 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 objective, pero suponiendo que no puede usar lock(this){return mydateetc;} , puede reconsiderar el enfoque; con más información, estoy seguro de que Stack Overflow puede ayudar 😉

Yo personalmente uso la statement “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 stack de llamadas y normalmente terminan el procesamiento con un mensaje apropiado (el error y el seguimiento de la stack) 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 scope 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. .

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.

“Montón de propiedades” y el locking en el captador de propiedades y el nivel del instalador se ve mal. Su locking es demasiado fino. En la mayoría de los usos típicos de objetos, querrá asegurarse de haber adquirido un locking 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 locking que tendrá que escribir.

DRY dice: segunda solución. La primera solución duplica la lógica de usar un locking, mientras que la segunda no.

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

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 locking no tiene que volver a escribirse varias veces.

Si bien estoy de acuerdo con muchos de los comentarios anteriores, incluida la granularidad del locking 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 locking. ¿Porque es esto importante?

Nos ocupamos de muchos mecanismos y comportamientos de locking diferentes. Su objeto de locking puede tener un tiempo de espera específico que emplea. La implementación de su locking puede ser un locking de monitor, un locking de lector, un locking de escritor o un locking 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 locking 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.

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 locking 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(); } } } 

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 locking 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(); } } } 

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 locking 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 scope y porque el Framework asegurará el uso correcto de “IDisposable” (este último es menos beneficioso si está implementando “IDisposabe”).

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() { ///  /// Allows you to enter and exit a read lock with a using statement ///  /// The lock /// A new object that will ExitReadLock on dispose 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()); } ///  /// Allows you to enter and exit a write lock with a using statement ///  /// The lock /// A new object that will ExitWriteLock on dispose 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()); } } ///  /// Calls the finished action on dispose. For use with a using statement. ///  public class OnDispose : IDisposable { Action _finished; public OnDispose(Action finished) { _finished = finished; } public void Dispose() { _finished(); } } 

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(); } } }