Durante el cambio a los nuevos .NET Core 3 IAsynsDisposable, me encontré con el siguiente problema.
El núcleo del problema: si DisposeAsyncarroja una excepción, esta excepción oculta cualquier excepción lanzada dentro de await using-block.
class Program
{
static async Task Main()
{
try
{
await using (var d = new D())
{
throw new ArgumentException("I'm inside using");
}
}
catch (Exception e)
{
Console.WriteLine(e.Message); // prints I'm inside dispose
}
}
}
class D : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await Task.Delay(1);
throw new Exception("I'm inside dispose");
}
}
Lo que queda atrapado es la AsyncDisposeexcepción-si se lanza, y la excepción desde adentro await usingsolo si AsyncDisposeno se lanza.
Sin embargo, preferiría lo contrario: obtener la excepción del await usingbloque si es posible, y DisposeAsync-excepción solo si el await usingbloque terminó con éxito.
Justificación: Imagine que mi clase Dtrabaja con algunos recursos de red y se suscribe a algunas notificaciones remotas. El código interno await usingpuede hacer algo mal y fallar el canal de comunicación, luego el código en Dispose que intenta cerrar la comunicación con gracia (por ejemplo, cancelar la suscripción a las notificaciones) también fallará. Pero la primera excepción me da la información real sobre el problema, y la segunda es solo un problema secundario.
En el otro caso, cuando la parte principal se ejecutó y la eliminación falló, el verdadero problema está dentro DisposeAsync, por lo que la excepción DisposeAsynces la relevante. Esto significa que suprimir todas las excepciones en el interior DisposeAsyncno debería ser una buena idea.
Sé que existe el mismo problema con el caso no asíncrono: la excepción en finallyanula la excepción en try, por eso no se recomienda incluir Dispose(). Pero con las clases de acceso a la red que suprimen las excepciones en los métodos de cierre no se ve nada bien.
Es posible solucionar el problema con el siguiente ayudante:
static class AsyncTools
{
public static async Task UsingAsync<T>(this T disposable, Func<T, Task> task)
where T : IAsyncDisposable
{
bool trySucceeded = false;
try
{
await task(disposable);
trySucceeded = true;
}
finally
{
if (trySucceeded)
await disposable.DisposeAsync();
else // must suppress exceptions
try { await disposable.DisposeAsync(); } catch { }
}
}
}
y úsalo como
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
lo cual es un poco feo (y no permite cosas como retornos tempranos dentro del bloque de uso).
¿Existe una buena solución canónica, await usingsi es posible? Mi búsqueda en internet no encontró ni siquiera discutir este problema.

Closemétodo separado por esta misma razón. Probablemente sea aconsejable hacer lo mismo:CloseAsyncintenta cerrar las cosas muy bien y arroja el fracaso.DisposeAsyncsimplemente hace lo mejor y falla en silencio.CloseAsyncmedio separado significa que debo tomar precauciones adicionales para que funcione. Si lo pongo al final deusing-block, se omitirá en los primeros retornos, etc. (esto es lo que queremos que suceda) y excepciones (esto es lo que queremos que suceda). Pero la idea parece prometedora.Disposesiempre ha sido "Las cosas podrían haber salido mal: solo haz tu mejor esfuerzo para mejorar la situación, pero no la empeores", y no veo por quéAsyncDisposedebería ser diferente.DisposeAsynclo mejor para ordenar pero no tirar es lo correcto. Estaba hablando de devoluciones anticipadas intencionales , donde una devolución anticipada intencional podría omitir por error una llamada aCloseAsync: esas son las prohibidas por muchos estándares de codificación.Respuestas:
Hay excepciones que desea mostrar (interrumpir la solicitud actual o reducir el proceso), y hay excepciones que su diseño espera que ocurran a veces y puede manejarlas (por ejemplo, vuelva a intentarlo y continúe).
Pero distinguir entre estos dos tipos depende de la persona que realiza la última llamada del código; este es el punto principal de las excepciones, dejar la decisión a la persona que llama.
A veces, la persona que llama dará mayor prioridad a la aparición de la excepción del bloque de código original, y a veces la excepción del
Dispose. No existe una regla general para decidir cuál debe tener prioridad. El CLR es al menos consistente (como ha notado) entre el comportamiento sincronizado y el no asíncrono.Tal vez sea desafortunado que ahora tengamos
AggregateExceptionque representar múltiples excepciones, no se puede adaptar para resolver esto. es decir, si una excepción ya está en vuelo y se lanza otra, se combinan en unAggregateException. Elcatchmecanismo podría modificarse para que, si escribecatch (MyException), capture cualquieraAggregateExceptionque incluya una excepción de tipoMyException. Sin embargo, hay varias otras complicaciones derivadas de esta idea, y probablemente sea demasiado arriesgado modificar algo tan fundamental ahora.Podría mejorar su
UsingAsyncpara respaldar la devolución anticipada de un valor:fuente
await usingse puede usar estándar (aquí es donde DisposeAsync no arrojará un caso no fatal), y un ayudante comoUsingAsynces más apropiado (si es probable que DisposeAsync arroje) ? (Por supuesto, tendría que modificarUsingAsyncpara que no capte todo a ciegas, pero solo no sea fatal (y no descabellado en el uso de Eric Lippert ).)Quizás ya entiendas por qué sucede esto, pero vale la pena explicarlo. Este comportamiento no es específico para
await using. También sucedería con unusingbloque simple . Entonces, mientras digoDispose()aquí, todo se aplicaDisposeAsync()también.Un
usingbloque es solo azúcar sintáctico para untry/finallybloque, como dice la sección de comentarios de la documentación . Lo que ves sucede porque elfinallybloque siempre se ejecuta, incluso después de una excepción. Entonces, si ocurre una excepción y no haycatchbloque, la excepción se pone en espera hastafinallyque se ejecuta el bloque y luego se lanza la excepción. Pero si ocurre una excepciónfinally, nunca verá la excepción anterior.Puedes ver esto con este ejemplo:
No importa si
Dispose()oDisposeAsync()se llama dentro de lafinally. El comportamiento es el mismo.Mi primer pensamiento es: no tirar
Dispose(). Pero después de revisar algunos de los códigos propios de Microsoft, creo que depende.Eche un vistazo a su implementación de
FileStream, por ejemplo. Tanto elDispose()método sincrónico , y enDisposeAsync()realidad puede lanzar excepciones. El síncronaDispose()hace caso omiso de algunas excepciones intencionalmente, pero no todos.Pero creo que es importante tener en cuenta la naturaleza de tu clase. En un
FileStream, por ejemplo,Dispose()vaciará el búfer al sistema de archivos. Esa es una tarea muy importante y necesita saber si eso falló . No puedes ignorar eso.Sin embargo, en otros tipos de objetos, cuando llamas
Dispose(), ya no tienes uso para el objeto. LlamarDispose()realmente solo significa "este objeto está muerto para mí". Tal vez limpia parte de la memoria asignada, pero la falla no afecta el funcionamiento de su aplicación de ninguna manera. En ese caso, puede decidir ignorar la excepción dentro de suDispose().Pero en cualquier caso, si desea distinguir entre una excepción dentro
usingo una excepción que provieneDispose(), entonces necesita untry/catchbloque tanto dentro como fuera de suusingbloque:O simplemente no podrías usar
using. Escriba atry/catch/finallyblock usted mismo, donde detecte cualquier excepción enfinally:fuente
catchinterior delusingbloque no ayudaría porque, por lo general, el manejo de excepciones se realiza en algún lugar lejos delusingbloque en sí, por lo que nousinges muy práctico manejarlo dentro . Sobre el uso de nousing, ¿es realmente mejor que la solución propuesta?try/catch/finallybloquear ya que sería claro de inmediato lo que está haciendo sin tener que ir a leer lo queAsyncUsingestá haciendo. También tiene la opción de hacer un regreso temprano. También habrá un costo adicional de CPU para suAwaitUsing. Sería pequeño, pero está ahí.Dispose()no se debe tirar porque se llama más de una vez. Las propias implementaciones de Microsoft pueden arrojar excepciones, y por una buena razón, como he mostrado en esta respuesta. Sin embargo, estoy de acuerdo en que debe evitarlo si es posible, ya que nadie esperaría que se lance normalmente.usar es efectivamente el Código de manejo de excepciones (sintaxis de azúcar para probar ... finalmente ... Dispose ()).
Si su código de manejo de excepciones arroja Excepciones, algo está realmente roto.
Cualquier otra cosa que te haya metido allí, ya no importa. El código de manejo de excepciones defectuosas ocultará todas las excepciones posibles, de una forma u otra. El código de manejo de excepciones debe ser fijo, eso tiene prioridad absoluta. Sin eso, nunca obtienes suficientes datos de depuración para el problema real. Veo que se hace mal extremadamente a menudo. Es tan fácil equivocarse como manejar punteros desnudos. Muy a menudo, hay dos artículos sobre el tema temático que enlace, que pueden ayudarlo con cualquier concepción errónea subyacente:
Dependiendo de la clasificación de excepciones, esto es lo que debe hacer si su código de manejo de excepciones / dipose produce una excepción:
Para Fatal, Boneheaded y Vexing, la solución es la misma.
Excepciones exógenas, deben evitarse incluso a un costo serio. Hay una razón por la que todavía utilizamos archivos de registro en lugar de bases de datos de registro para registrar excepciones: las operaciones de base de datos DB son solo una forma de ser propensos a tener problemas exógenos. Los archivos de registro son el único caso, donde ni siquiera me importa si mantiene el controlador de archivo abierto durante todo el tiempo de ejecución.
Si tienes que cerrar una conexión, no te preocupes demasiado por el otro extremo. Manéjelo como lo hace UDP: "Enviaré la información, pero no me importa si la otra parte la obtiene". Desechar es limpiar recursos del lado del cliente en el que está trabajando.
Puedo intentar notificarles. ¿Pero limpiar cosas en el lado del servidor / FS? De eso son responsables sus tiempos de espera y su manejo de excepciones.
fuente
NativeMethods.UnmapViewOfFile();yNativeMethods.CloseHandle(). Pero esos son importados desde el exterior. No se verifica ningún valor de retorno o cualquier otra cosa que pueda usarse para obtener una Excepción .NET adecuada en torno a lo que puedan encontrar esos dos. Así que estoy asumiendo firmemente que SqlConnection.Dispose (bool) simplemente no le importa. El | Cerrar es mucho mejor, en realidad le dice al servidor. Antes de que llame disponga.Puede intentar usar AggregateException y modificar su código de la siguiente manera:
https://docs.microsoft.com/ru-ru/dotnet/api/system.aggregateexception?view=netframework-4.8
https://docs.microsoft.com/ru-ru/dotnet/standard/parallel-programming/exception-handling-task-parallel-library
fuente