Lors du passage aux nouveaux .NET Core 3 IAsynsDisposable
, je suis tombé sur le problème suivant.
Le cœur du problème: si DisposeAsync
lève une exception, cette exception cache toutes les exceptions await using
levées à l' intérieur de -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");
}
}
Ce qui se fait attraper, c'est l' AsyncDispose
exception-si elle est lancée, et l'exception de l'intérieur await using
uniquement si AsyncDispose
elle ne lance pas.
Je préfère cependant l'inverse: obtenir l'exception du await using
bloc si possible, et DisposeAsync
-exception uniquement si le await using
bloc s'est terminé avec succès.
Justification: Imaginez que ma classe D
fonctionne avec certaines ressources réseau et s'abonne à certaines notifications à distance. Le code à l'intérieur await using
peut faire quelque chose de mal et échouer le canal de communication, après quoi le code dans Dispose qui essaie de fermer la communication avec élégance (par exemple, se désinscrire des notifications) échouera également. Mais la première exception me donne les vraies informations sur le problème, et la seconde n'est qu'un problème secondaire.
Dans l'autre cas, lorsque la partie principale a traversé et que l'élimination a échoué, le vrai problème est à l'intérieur DisposeAsync
, donc l'exception de DisposeAsync
est pertinente. Cela signifie que la suppression de toutes les exceptions à l'intérieur DisposeAsync
ne devrait pas être une bonne idée.
Je sais qu'il y a le même problème avec le cas non asynchrone: l'exception dans finally
remplace l'exception dans try
, c'est pourquoi il n'est pas recommandé de le faire Dispose()
. Mais avec les classes d'accès au réseau, la suppression des exceptions dans les méthodes de fermeture ne semble pas du tout bonne.
Il est possible de contourner le problème avec l'aide suivante:
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 { }
}
}
}
et l'utiliser comme
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
ce qui est un peu moche (et interdit les choses comme les premiers retours à l'intérieur du bloc using).
Existe-t-il une bonne solution canonique, avec await using
si possible? Ma recherche sur Internet n'a même pas permis de discuter de ce problème.
Close
méthode distincte pour cette raison. Il est probablement sage de faire de même:CloseAsync
tente de bien fermer les choses et jette l'échec.DisposeAsync
fait de son mieux et échoue silencieusement.CloseAsync
moyen séparé , je dois prendre des précautions supplémentaires pour le faire fonctionner. Si je le mets juste à la fin deusing
-block, il sera ignoré lors des premiers retours, etc. Mais l'idée semble prometteuse.Dispose
a toujours été "Les choses ont peut-être mal tourné: faites simplement de votre mieux pour améliorer la situation, mais ne faites pas qu'aggraver", et je ne vois pas pourquoi celaAsyncDispose
devrait être différent.DisposeAsync
faire de son mieux pour ranger mais pas jeter est la bonne chose à faire. Vous parliez de retours anticipés intentionnels , où un retour anticipé intentionnel pourrait par erreur contourner un appel àCloseAsync
: ce sont ceux qui sont interdits par de nombreuses normes de codage.Réponses:
Il existe des exceptions que vous souhaitez faire apparaître (interrompre la demande en cours ou interrompre le processus), et il existe des exceptions qui, selon votre conception, se produiront parfois et vous pouvez les gérer (par exemple, réessayer et continuer).
Mais la distinction entre ces deux types appartient à l'appelant ultime du code - c'est tout l'intérêt des exceptions, de laisser la décision à l'appelant.
Parfois, l'appelant accordera une plus grande priorité à la détection de l'exception du bloc de code d'origine, et parfois de l'exception du
Dispose
. Il n'y a pas de règle générale pour décider laquelle devrait être prioritaire. Le CLR est au moins cohérent (comme vous l'avez noté) entre le comportement de synchronisation et non asynchrone.Il est peut-être regrettable que nous devions maintenant
AggregateException
représenter plusieurs exceptions, il ne peut pas être modifié pour résoudre ce problème. c'est-à-dire que si une exception est déjà en vol et qu'une autre est levée, elles sont combinées en unAggregateException
. Lecatch
mécanisme pourrait être modifié de sorte que si vous écrivez,catch (MyException)
il interceptera tout ceAggregateException
qui inclut une exception de typeMyException
. Il existe cependant diverses autres complications découlant de cette idée, et il est probablement trop risqué de modifier quelque chose de si fondamental maintenant.Vous pouvez améliorer votre
UsingAsync
pour prendre en charge le retour précoce d'une valeur:la source
await using
peut être utilisé (c'est là que DisposeAsync ne lancera pas dans un cas non fatal), et un assistant commeUsingAsync
est plus approprié (si DisposeAsync est susceptible de lancer) ? (Bien sûr, je devrais le modifierUsingAsync
pour qu'il n'attrape pas tout aveuglément, mais seulement non fatal (et non sans tête dans l'utilisation d'Eric Lippert ).)Peut-être que vous comprenez déjà pourquoi cela se produit, mais cela vaut la peine d'être expliqué. Ce comportement n'est pas spécifique à
await using
. Cela arriverait aussi avec unusing
bloc simple . Donc, bien que je disDispose()
ici, tout cela s'appliqueDisposeAsync()
aussi.Un
using
bloc est juste du sucre syntaxique pour un bloctry
/finally
, comme le dit la section des remarques de la documentation . Ce que vous voyez se produit car lefinally
bloc s'exécute toujours , même après une exception. Donc, si une exception se produit et qu'il n'y a pas decatch
bloc, l'exception est mise en attente jusqu'à ce que lefinally
bloc s'exécute, puis l'exception est levée. Mais si une exception se produitfinally
, vous ne verrez jamais l'ancienne exception.Vous pouvez le voir avec cet exemple:
Peu importe si
Dispose()
ouDisposeAsync()
est appelé à l'intérieur dufinally
. Le comportement est le même.Ma première pensée est: ne jetez pas
Dispose()
. Mais après avoir examiné une partie du code de Microsoft, je pense que cela dépend.Jetez un œil à leur mise en œuvre
FileStream
, par exemple. Les deux laDispose()
méthode synchrone , etDisposeAsync()
peuvent réellement lever des exceptions. La synchronisationDispose()
ne fait ignorer quelques exceptions intentionnellement, mais pas tous.Mais je pense qu'il est important de prendre en compte la nature de votre classe. Dans un
FileStream
, par exemple,Dispose()
videra le tampon dans le système de fichiers. C'est une tâche très importante et vous devez savoir si cela a échoué . Vous ne pouvez pas simplement ignorer cela.Cependant, dans d'autres types d'objets, lorsque vous appelez
Dispose()
, vous n'avez vraiment plus besoin de l'objet. AppelerDispose()
signifie simplement "cet objet est mort pour moi". Peut-être qu'il nettoie une partie de la mémoire allouée, mais l'échec n'affecte en rien le fonctionnement de votre application. Dans ce cas, vous pourriez décider d'ignorer l'exception à l'intérieur de votreDispose()
.Mais dans tous les cas, si vous voulez faire la distinction entre une exception à l'intérieur de
using
ou une exception qui vient deDispose()
, alors vous avez besoin d'un bloctry
/catch
à l'intérieur et à l'extérieur de votreusing
bloc:Ou vous ne pourriez tout simplement pas utiliser
using
. Écrivez vous-même un bloctry
/catch
/finally
, où vous interceptez toute exception dansfinally
:la source
catch
intérieur duusing
bloc n'aiderait pas, car la gestion des exceptions se fait généralement quelque part loin duusing
bloc lui-même, donc sa manipulation à l'intérieurusing
n'est généralement pas très pratique. À propos de l'utilisation de nonusing
- est-ce vraiment mieux que la solution de contournement proposée?try
/catch
/finally
block car il serait immédiatement clair de ce qu'il fait sans avoir à lire ce qu'ilAsyncUsing
fait. Vous gardez également la possibilité de faire un retour anticipé. Il y aura également un coût supplémentaire pour votre CPUAwaitUsing
. Ce serait petit, mais il est là.Dispose()
ne faut pas lancer car il est appelé plus d'une fois. Les propres implémentations de Microsoft peuvent lever des exceptions, et pour une bonne raison, comme je l'ai montré dans cette réponse. Cependant, je suis d'accord que vous devriez l'éviter si possible car personne ne s'attendrait normalement à ce qu'il lance.l'utilisation est en fait un code de gestion des exceptions (sucre de syntaxe pour essayer ... enfin ... Dispose ()).
Si votre code de gestion des exceptions lève des exceptions, quelque chose est royalement explosé.
Tout ce qui s'est passé pour vous y amener, n'a plus vraiment d'importance. Un code de gestion des exceptions défectueux masquera toutes les exceptions possibles, dans un sens ou dans l'autre. Le code de gestion des exceptions doit être fixe, avec une priorité absolue. Sans cela, vous n'obtenez jamais suffisamment de données de débogage pour le vrai problème. Je le vois très souvent mal. Il est aussi facile de se tromper que de manipuler des pointeurs nus. Si souvent, il y a deux articles sur le lien thématique I, qui pourraient vous aider avec toute conception erronée sous-jacente:
En fonction de la classification des exceptions, voici ce que vous devez faire si votre code de gestion des exceptions / dipose lève une exception:
Pour Fatal, Boneheaded et Vexing, la solution est la même.
Les exceptions exogènes doivent être évitées même à un coût élevé. Il y a une raison pour laquelle nous utilisons toujours des fichiers journaux plutôt que des bases de données pour consigner les exceptions - les opérations DB sont juste un moyen de s'exposer à des problèmes exogènes. Les fichiers journaux sont le seul cas, où cela ne me dérange même pas si vous gardez la poignée de fichier ouverte pendant toute la durée d'exécution.
Si vous devez fermer une connexion, ne vous inquiétez pas trop de l'autre extrémité. Traitez-le comme le fait UDP: "J'enverrai les informations, mais je m'en fiche si l'autre côté les obtienne." L'élimination consiste à nettoyer les ressources côté client / côté sur lequel vous travaillez.
Je peux essayer de les informer. Mais nettoyer les choses côté serveur / FS? C'est de cela que leurs délais d'attente et leur gestion des exceptions sont responsables.
la source
NativeMethods.UnmapViewOfFile();
etNativeMethods.CloseHandle()
. Mais ceux-ci sont importés depuis extern. Il n'y a aucune vérification de toute valeur de retour ou de toute autre chose qui pourrait être utilisée pour obtenir une exception .NET appropriée autour de ce que ces deux pourraient rencontrer. Je suppose donc fortement que SqlConnection.Dispose (bool) s'en fiche tout simplement. | Fermer est beaucoup plus agréable à dire au serveur. Avant d'appeler disposer.Vous pouvez essayer d'utiliser AggregateException et modifier votre code comme ceci:
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
la source