Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.
Disposer des objets de façon asynchrone
Finalize() et Dispose()
IAsyncDisposable
Utilisation de using sans {…}
Avec await using
Avec ConfigureAwait()
C# 8.0 apporte un cas d’utilisation supplémentaire au pattern async/await en permettant de disposer des objets de façon asynchrone avec using
(i.e. asynchronous disposable).
Rappels sur async/await
Pour rappel async/await
est apparu avec C# 5 de façon à faciliter l’implémentation de traitements asynchrones. Brièvement la syntaxe de cette implémentation doit être utilisée de cette façon:
async
est placé dans la signature de la fonction comportement un traitement pouvait être exécuté de façon asynchrone.await
est placé devant l’instruction générant une tâche dont l’exécution est lancée et dont il faudra attendre la fin de l’exécution. L’instruction doit générer un objet dont le type est “awaitable” commeTask
,Task<T>
,ValueTask
ouValueTask<T>
(ValueTask
etValueTask<T>
apparus à partir de C# 7.0 sont des objets de type valeur équivalent àTask
etTask<T>
).- Dans une méthode asynchrone, tout ce qui se trouve après l’instruction
await
sera exécutée sous la forme d’une continuation. - Une méthode dont le corps contient une ou plusieurs instructions avec
await
doit comporterasync
dans sa signature. - Si une méthode contient
async
, il n’est pas obligatoire que le corps de la méthode contienneawait
. Si une méthodeasync
ne contient aucune instruction avecawait
, l’exécution sera effectuée de façon synchrone.
Par exemple, si on considère la fonction suivante:
public async Task ExecuteAsynchronously()
{
await Task.Delay(10000); // On simule l'exécution d'un traitement dans une Task
}
On peut appeler cette méthode de cette façon:
// La méthode appelante doit comporter async si on utilise await
public async Task CallingMethod()
{
await ExecuteAsynchronously();
}
Ou
// async n'est pas nécessaire si on n'utilise pas await
public void CallingMethod()
{
// On ne fait qu'attendre le résultat d'une Task
ExecuteAsynchronously().Wait();
// ou
ExecuteAsynchronously().Result;
}
Le code dans ExecuteAsynchronously()
est exécuté de façon asynchrone par rapport au code se trouvant à l’extérieur de ExecuteAsynchronously()
. Cela signifie que:
- L’appel
await ExecuteAsynchronously()
rendra la main immédiatement au code appelant de façon à ne pas bloquer son exécution. - Le code dans
ExecuteAsynchronously()
sera exécuté dans une Task différente du code appelant. Concrêtement et suivant le contexte d’exécution, cette Task pourra être exécutée dans un thread différent du thread appelant mais ce n’est pas obligatoire. - Dans l’appel,
await
signifie que le code appelant va attendre la fin de l’exécution de la Task dansExecuteAsynchronously()
pour continuer l’exécution.
Exécution asynchrone ne signifie pas forcément que l’exécution s’effectue en parallèle:
- Exécution asynchrone signifie que les exécutions de tâches sont effectuées sans qu’elles ne soient synchronisées. Une exécution de tâches asynchrones peut être effectuée par un seul thread de façon séquentielle.
- Exécution en parallèle signifie que des tâches peuvent être exécutées éventuellement dans des threads différents et tirer parti d’un processeur multithreadé ou d’une machine multiprocesseurs.
Si on souhaite exécuter du code de façon concurrente avec le code dans ExecuteAsynchronously()
, on peut utiliser la syntaxe suivante:
Task asyncTask = ExecuteAsynchronously();
// Code exécuté de façon concurrente
// ...
await asyncTask;
// Code exécuté sous forme de continuation
// ...
On peut se rendre de l’aspect asynchrone de l’exécution de code avec await
dans le cas d’une implémentation d’une interface graphique avec une boucle de messages comme en WPF ou en WinForms:
- Si on lance des traitements de façon synchrone dans le thread graphique, la boucle de message de l’interface est bloquée et les éléments graphiques sont inaccessibles ou bloqués pendant le traitement.
- Si on lance des traitements de façon asynchrone avec
await
dans le thread graphique, la boucle de message est toujours exécutée et les éléments graphiques restent accessibles pendant le traitement.
Disposer des objets de façon asynchrone
Le garbage collector (GC) permet de gérer la durée de vie des objets managés d’un processus .NET: lorsqu’un objet n’apparait plus dans le graphe des objets indiquant les dépendances entre les objets, il est “supprimé”. Cette suppression se fait en récupérant l’espace mémoire occupé lors de l’étape Collect.
Finalize() et Dispose()
Avant l’étape Collect, il peut être nécessaire d’exécuter du code pour libérer des dépendances de l’objet à collecter:
- Méthode
Finalize()
: cette méthode n’est pas directement exposée au développeur. Le garbage collector l’exécute avant l’étape Collect. Une partie du code de cette méthode peut être personnalisé en implémentant un destructeur, par exemple pour libérer des dépendances non managées. En effet, le garbage collector n’intervient que sur les objets managés du processus, si le processus manipule des objets non managés, il faut les libérer explicitement. - Méthode
Dispose()
: cette méthode est implémentée dans les objets satisfaisant l’interfaceIDisposable
:public class DisposableObject : IDisposable { public void Dispose() { // ... } }
Le garbage collector n’exécute pas la méthode Dispose()
, elle doit être appelée explicitement par du code de façon à libérer des dépendances managées ou non managées d’un objet. L’appel à Dispose()
peut se faire en appelant directement cette méthode ou en utilisant using
:
using (<objet satisfaisant IDisposable>)
{
// ...
}
A la fin du bloc de code using
, la méthode Dispose()
de l’objet satisfaisant IDisposable
sera exécutée.
Par exemple:
var disposableObject = new DisposableObject();
using (disposableObject)
{
// ...
}
// ATTENTION: à ce stade disposableObject ne doit pas être utilisé car Dispose() a été exécuté
Ou plus directement:
using (var disposableObject = new DisposableObject())
{
// ...
}
L’exécution de la méthode Dispose()
ne dispense pas le garbage collector d’exécuter la méthode Finalize()
lorsque l’objet est libéré. Pour éviter au garbage collector d’exécuter inutilement Finalize()
, on peut rajouter GC.SuppressFinalize()
dans la méthode Dispose()
de façon à indiquer au GC qu’il n’est pas nécessaire d’exécuter Finalize()
.
Dans le cas où il est nécessaire de libérer des dépendances d’un objet, une bonne implémentation doit:
- Satisfaire
IDisposable
- L’implémentation de la méthode
Dispose()
doit indiquer au garbage collector de ne pas exécuterFinalize()
avecGC.SuppressFinalize()
, - Eventuellement libérer des dépendances managées et non managées dans la méthode
Dispose()
. - Comporter un destructeur si des dépendances non managées doivent être libérées. L’implémentation du destructeur peut être nécessaire si la méthode
Dispose()
n’est pas appelée explicitement dans tous les cas de figure. - Le destructeur peut libérer les dépendances non managées et supprimer les liens entre l’objet et d’autres objets managés en affectant
null
aux données membres. Supprimer ce lien permet au garbage collector de supprimer les instances inutiles dans le graphe des objets. - L’appel à
Dispose()
ne doit être fait qu’une seule fois: l’implémentation doit être idempotente c’est-à-dire qu’elle doit permettre d’être exécutée plusieurs fois mais les objets ne doivent être libérés qu’une seule fois. Ainsi avant d’exécuterDispose()
, il est nécessaire de vérifier que l’exécution n’a pas été déjà effectuée (c’est l’intérêt du membredisposed
dans l’implémentation deDisposableObject
plus bas).
Dans la documentation, on peut trouver un exemple d’implémentation prenant en compte tous ces éléments. Si on prend l’exemple d’une dépendance de type SqlConnection
à libérer (System.Data.SqlClient.SqlConnection
dérive de System.Data.Common.DbConnection
qui satisfait IDisposable
):
class DisposableObject : IDisposable // (1)
{
bool disposed = false;
private SqlConnection dependency = new SqlConnection("Test connection");
public void Dispose()
{
this.Dispose(true); // (3)
GC.SuppressFinalize(this); // (2)
}
private void Dispose(bool disposing)
{
if (this.disposed) // (6)
return;
if (disposing)
{
// Libération de dépendances managées
if (this.dependency != null)
this.dependency.Dispose();
}
// On supprime le lien entre DisposableObject et l’instance de SqlConnection.
this.dependency = null; // (5)
// Libération de dépendances non managées
// ...
this.disposed = true;
}
// Personnalisation de Finalize() si nécessaire
~DisposableObject() // (4)
{
this.Dispose(false); // (5)
}
}
Avec la syntaxe using
, l’exécution de Dispose()
est synchrone.
IAsyncDisposable
A partir de C# 8.0, il est possible d’exécuter du code permettant la libération de ressources de façon asynchrone en utilisant le pattern async/await. L’implémentation est similaire à celle avec IDisposable
:
- Il faut satisfaire
IAsyncDisposable
- Implémenter une méthode dont la signature est:
public ValueTask DisposeAsync();
Par exemple:
public class DisposableObject : IAsyncDisposable { public async ValueTask DisposeAsync() { // ... } }
La méthode
DisposeAsync()
doit retourner un objet de typeValueTask
qui est un objet de type valeur équivalent à l’objetTask
. - Utiliser la syntaxe
await using
pour que l’exécution de la méthodeDisposeAsync()
soit asynchrone:await using (<objet satisfaisant IAsyncDisposable>) { // ... }
Par exemple:
var disposableObject = new DisposableObject(); await using (disposableObject) { // ... } // ATTENTION: à ce stade disposableObject ne doit pas être utilisé car DisposeAsync() a été exécuté
Ou plus directement:
await using (var disposableObject = new DisposableObject()) { // ... }
Une autre syntaxe utilisant ConfigureAwait(false)
permet d’optimiser le code en évitant d’utiliser le contexte d’exécution d’origine lors de l’exécution de la continuation avec async/await
:
await using (<objet satisfaisant IAsyncDisposable>.ConfigureAwait(false))
{
// ...
}
Par exemple:
var disposableObject = new DisposableObject();
await using (disposableObject.ConfigureAwait(false))
{
// ...
}
Ou
var disposableObject = new DisposableObject();
await using (System.Runtime.CompilerServices.ConfiguredAsyncDisposable configuredDisposable =
disposableObject.ConfigureAwait(false))
{
// ...
}
// ATTENTION: à ce stade disposableObject ne doit pas être utilisé car DisposeAsync() a été exécuté
Comme pour IDisposable
, l’implémentation doit respecter quelques recommandations:
- Indiquer au garbage collector de ne pas exécuter
Finalize()
avecGC.SuppressFinalize()
, - De libérer des dépendances managées de façon asynchrone dans la méthode
DisposeAsync()
. - L’implémentation doit être idempotente.
Dans la documentation, on peut ainsi trouver un exemple d’implémentation. Si on prend l’exemple d’une dépendance de type SqlConnection
à libérer (System.Data.SqlClient.SqlConnection
dérive de System.Data.Common.DbConnection
qui satisfait IAsyncDisposable
):
class DisposableAsyncObject : IAsyncDisposable
{
private SqlConnection dependency = new SqlConnection("Test connection");
public async void DisposeAsync()
{
await this.DisposeAsyncCore(); // (b)
GC.SuppressFinalize(this); // (a)
}
private async ValueTask DisposeAsyncCore()
{
if (this.dependency != null)
await this.dependency.DisposeAsync();
this.dependency = null;
}
}
Dans le cas d’une implémentation satisfaisant IDisposable
et IAsyncDisposable
:
class DisposableAsyncObject : IAsyncDisposable, IDisposable
{
bool disposed = false;
private SqlConnection dependency = new SqlConnection("Test connection");
public async void DisposeAsync()
{
await this.DisposeAsyncCore(); // (b)
this.Dispose(false); // false pour ne pas disposer les dépendances de façon synchrone.
GC.SuppressFinalize(this); // (a)
}
public void Dispose()
{
this.Dispose(true); // (3)
GC.SuppressFinalize(this); // (2)
}
private async ValueTask DisposeAsyncCore()
{
if (this.dependency != null)
await this.dependency.DisposeAsync();
this.dependency = null;
}
private void Dispose(bool disposing)
{
if (this.disposed) // (c)
return;
if (disposing)
{
// Libération de dépendances managées de façon synchrone
if (this.dependency != null)
this.dependency.Dispose();
}
// On supprime le lien entre DisposableObject et l’instance de SqlConnection.
this.dependency = null; // (5)
// Libération de dépendances non managées
// ...
this.disposed = true;
}
// Personnalisation de Finalize() si nécessaire
~DisposableAsyncObject() // (4)
{
this.Dispose(false); // (5)
}
}
Quelques remarques supplémentaires concernant l’implémentation:
- L’implémentation asynchrone avec
IAsyncDisposable
ne remplace pas l’implémentation avecIDisposable
. La première méthode convient pour libérer des dépendances de façon asynchrone quand cela est possible. Si ce n’est pas possible, la meilleure implémentation est d’utiliser la version synchrone. - Si la classe implémente
IAsyncDisposable
, il n’est pas obligatoire d’implémenterIDisposable
. - Si on libère des dépendances de façon synchrone dans
DisposeAsync()
, ces exécutions seront synchrones et il n’y aura pas de libération asynchrone des dépendances.
Utilisation de using sans {…}
Avant C# 8.0, using
devait obligatoirement être suivi d’un bloc de code:
using (<objet satisfaisant IDisposable>)
{
// ...
}
C# 8.0 permet d’utiliser using
sans bloc de code. La portée de l’objet concerné par using
correspond au bloc de code dans lequel se trouve using
. La méthode Dispose()
sera exécutée à la sortie de ce bloc de code.
Par exemple, dans le cas d’une méthode:
public void UseDisposableObject()
{
using var disposableObject = new DisposableObject();
// Utilisation de disposableObject
// ...
// disposableObject.Dispose() est exécuté juste avant la sortie de la méthode
}
Dans le cas d’un bloc de code:
public void UseDisposableObject()
{
{
using var disposableObject = new DisposableObject();
// Utilisation de disposableObject
// ...
// disposableObject.Dispose() est exécuté juste avant la sortie du bloc
}
// A ce niveau disposableObject est hors de portée
}
Avec await using
await using
est compatible avec cette syntaxe:
public void UseDisposableObject()
{
await using var disposableAsyncObject = new DisposableAsyncObject();
// Utilisation de disposableAsyncObject
// ...
// disposableAsyncObject.DisposeAsync() est exécuté juste avant la sortie de la méthode
}
Avec ConfigureAwait()
Avec ConfigureAwait()
, on peut utiliser la syntaxe:
var disposableObject = new DisposableObject();
await using System.Runtime.CompilerServices.ConfiguredAsyncDisposable useless =
disposableObject.ConfigureAwait(false);
Ou plus simplement:
await using var _ = disposableObject.ConfigureAwait(false);
DisposeAsync()
peut ne pas être exécuté en cas d’exception avec ConfigureAwait(false)
Suivant le pattern utilisé dans le cas où on utilise 2 objets satisfaisant IAsyncDisposable
, DisposeAsync()
peut ne pas être exécuté.
Par exemple, si on considère la classe suivante:
public class DisposableAsyncObject: IAsyncDisposable
{
public DisposableAsyncObject(int id)
{
this.Id = id;
}
public int Id { get; }
public async ValueTask DisposeAsync()
{
Console.WriteLine($"Instance {this.Id} of DisposableAsyncObject disposed");
}
}
Si on exécute le code suivant dans lequel une exception survient:
var instanceOne = new DisposeAsyncObject(1);
var instanceTwo = new DisposeAsyncObject(2);
// On lance volontairement une exception
if (instanceTwo.Id == 2)
throw new InvalidOperationException();
await using var uselessVarOne = instanceOne.ConfigureAwait(false);
await using var uselessVarTwo = instanceTwo.ConfigureAwait(false);
Console.WriteLine(instanceOne.Id);
Console.WriteLine(instanceTwo.Id);
A l’exécution, on peut se rendre compte qu’aucune des 2 instances n’est disposée car les lignes await
ne sont pas atteintes au moment de l’exception:
Unhandled exception. SystemOperationException: ...
En utilisant await using
juste après l’instanciation de l’objet instanceOne
, DisposeAsync()
sera exécuté même si l’exception survient:
var instanceOne = new DisposeAsyncObject(1);
await using var uselessVarOne = instanceOne.ConfigureAwait(false);
var instanceTwo = new DisposeAsyncObject(2);
// On lance volontairement une exception
if (instanceTwo.Id == 2)
throw new InvalidOperationException();
await using var uselessVarTwo = instanceTwo.ConfigureAwait(false);
Console.WriteLine(instanceOne.Id);
Console.WriteLine(instanceTwo.Id);
instanceOne.DisposeAsync()
est exécutée:
Instance 1 of DisposableAsyncObject disposed Unhandled exception. SystemOperationException: ...
Imbriquer les blocs await using
permet d’exécuter DisposeAsync()
dans le cas des 2 instances:
var instanceOne = new DisposeAsyncObject(1);
await using (var uselessVarOne = instanceOne.ConfigureAwait(false))
{
var instanceTwo = new DisposeAsyncObject(2);
await using (var uselessVarTwo = instanceTwo.ConfigureAwait(false))
{
// On lance volontairement une exception
if (instanceTwo.Id == 2)
throw new InvalidOperationException();
Console.WriteLine(instanceOne.Id);
Console.WriteLine(instanceTwo.Id);
}
}
DisposeAsync()
est exécuté pour les 2 instances:
Instance 1 of DisposableAsyncObject disposed Instance 2 of DisposableAsyncObject disposed Unhandled exception. SystemOperationException: ...
- Interface IDisposable: https://docs.microsoft.com/fr-fr/dotnet/api/system.idisposable
- Interface IAsyncDisposable: https://docs.microsoft.com/fr-fr/dotnet/api/system.iasyncdisposable
- Méthode IAsyncDisposable.DisposeAsync(): https://docs.microsoft.com/fr-fr/dotnet/api/system.iasyncdisposable.disposeasync#System_IAsyncDisposable_DisposeAsync
- Implémenter une méthode DisposeAsync(): https://docs.microsoft.com/fr-fr/dotnet/standard/garbage-collection/implementing-disposeasync
- C# 8 understanding await using syntax: https://stackoverflow.com/questions/58791938/c-sharp-8-understanding-await-using-syntax
- What is the difference between using and await using? And how can I decide which one to use?: https://stackoverflow.com/questions/58610350/what-is-the-difference-between-using-and-await-using-and-how-can-i-decide-which
- De la bonne utilisation de Async/Await en C#: https://www.e-naxos.com/Blog/post/De-la-bonne-utilisation-de-AsyncAwait-en-C.aspx
- How and when to use ‘async’ and ‘await’: https://stackoverflow.com/questions/14455293/how-and-when-to-use-async-and-await