Disposer des objets de façon asynchrone (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

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” comme Task, Task<T>, ValueTask ou ValueTask<T> (ValueTask et ValueTask<T> apparus à partir de C# 7.0 sont des objets de type valeur équivalent à Task et Task<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 comporter async dans sa signature.
  • Si une méthode contient async, il n’est pas obligatoire que le corps de la méthode contienne await. Si une méthode async ne contient aucune instruction avec await, 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 dans ExecuteAsynchronously() 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’interface IDisposable:
    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:

  1. Satisfaire IDisposable
  2. L’implémentation de la méthode Dispose() doit indiquer au garbage collector de ne pas exécuter Finalize() avec GC.SuppressFinalize(),
  3. Eventuellement libérer des dépendances managées et non managées dans la méthode Dispose().
  4. 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.
  5. 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.
  6. 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écuter Dispose(), 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 membre disposed dans l’implémentation de DisposableObject 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

C# 8.0

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 type ValueTask qui est un objet de type valeur équivalent à l’objet Task.

  • Utiliser la syntaxe await using pour que l’exécution de la méthode DisposeAsync() 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:

  1. Indiquer au garbage collector de ne pas exécuter Finalize() avec GC.SuppressFinalize(),
  2. De libérer des dépendances managées de façon asynchrone dans la méthode DisposeAsync().
  3. 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 avec IDisposable. 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émenter IDisposable.
  • 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 {…}

C# 8.0

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

Leave a Reply