ValueTask (C# 7)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 7 (i.e. C# 7.0/7.1/7.2/7.3).

A partir de C# 7.0, quelques améliorations ont été effectuées concernant les Task de façon à augmenter les performances en diminuant les allocations qui sont faites dans le tas managé.

Avant de commencer…

Les objets Task apparus avec le framework .NET 4, offrent une grande flexibilité pour l’exécution de traitement asynchrone (Utilisation des “Task” en 5 min):

On peut simplement créer une Task et attendre la fin de son traitement:

Task task = Task.Run({ 
    // ... 
    // Traitement asynchrone
}); 

// Autre traitement 
// ...
task.Wait(); // Attente de la fin du traitement

On peut récupérer le résultat si le traitement le nécessite:

Task taskWithResult = Task.Run({ 
    // ... 

    return 42; 
}); 

// ... 
int result = taskWithResult.Result; 
// ou 
int result = taskWithResult.GetAwaiter().GetResult(); 

Il est possible d’effectuer des traitements suivant la façon dont s’est déroulée l’exécution de la Task (i.e. continuation, par exemple:

Task continuationTask = task.ContinueWith(() => { 
    // Autre traitement 
}, TaskContinuationOptions.OnOnRanToCompletion); 

async/await

Avec C# 5 est apparu les mot-clés async/await pour faciliter l’implémentation de traitements asynchrones: tout ce qui se trouve après await est considéré comme une continuation, par exemple:

public static async Task ExecuteAsynchronously() 
{ 
    await Task.Run(() => { 
        // ... 
    }); 

    Console.WriteLine("Task completed"); 
} 

static void Main() 
{ 
    Task task = ExecuteAsynchronously(); 
    // ... 
    task.Wait(); 
}

ou avec la gestion de async dans la fonction Main() à partir de C# 7.1:

static async Task Main() 
{ 
    await ExecuteAsynchronously(); 
} 

La construction async/await permet de faciliter les traitements asynchrones en améliorant la syntaxe, par exemple:

public static async Task ExecuteAsynchronously() 
{ 
    Task runningTask Task.Run(() => { 
        // ... 
    }); 

    // Autres traitements concurrents effectués en même temps que runningTask. 
    await runningTask; 

    Console.WriteLine("Task completed"); 
} 

Ainsi, si le mot-clé async est présent dans une fonction mais qu’elle ne comporte pas await, un warning est généré pour indiquer qu’il n’y a pas d’utilisation de await pour indiquer une opération asynchrone non bloquante.

En revanche l’utilisation d’await dans une fonction impose l’utilisation d’async dans sa signature pour signaler la présence d’un traitement asynchrone.

Pour utiliser await avec une fonction, il faut que le retour de cette fonction soit un type “Awaitable” comme Task/Task<T> ou ValueTask/ValueTask<T>.

FromResult(), FromException() et FromCanceled()

Un statut est associé à une tâche lors de la durée de vie d’une instance, par exemple:

  • TaskStatus.Running pour une tâche en cours d’exécution,
  • TaskStatus.Faulted si une exception a été déclenchée lors de l’exécution,
  • TaskStatus.Canceled si un signal d’interruption a été lancé à partir d’un CancellationToken.
  • TaskStatus.RanToCompletion si la tâche a terminé son exécution correctement.

Il est possible de tester ces statuts d’exécution à partir de l’objet Task:

  • task.IsCanceled pour tester l’annulation,
  • task.IsCompleted pour vérifier si l’exécution est terminée
  • task.IsCompletedSuccessfully pour vérifier si l’exécution s’est terminée correctement.
  • task.IsFaulted si une exception s’est produite.

Certaines implémentations peuvent nécessiter de devoir renvoyer un statut d’exécution concernant une tâche dans le cas où l’exécution est annulée ou si une exception est survenue. A partir du framework .NET 4.6 est apparu:

  • Task.FromResult() pour créer un objet Task directement avec le résultat et avec le statut TaskStatus.RanToCompletion.
  • Task.FromException() pour créer un objet Task dont le statut est TaskStatus.Faulted.
  • Task.FromCancelled() pour créer un objet Task dont le statut est TaskStatus.Canceled.

Le but de Task.FromResult(), Task.FromException() et Task.FromCanceled() est de faciliter le respect des signatures de fonctions renvoyant un objet Task ou Task<T>.

Par exemple:

public Task GetResult(CancellationToken cancellationToken) 
{ 
    if (cancellationToken.IsCancellationRequested) 
    { 
        // Exécution annulée 
        return Task.FromCanceled(cancellationToken); 
    } 

    try 
    { 
        // Retour immédiat d’un résultat 
        return Task.FromResult(0); 
    } 
    catch (Exception exception) 
    { 
        // Retour avec une exception 
        return Task.FromException(exception); 
    } 

} 

Ces signatures de fonction renvoyant Task ou Task<T> permettent d’utiliser async/await:

public async Task GetResult(CancellationToken cancellationToken) 
{ 
    if (cancellationToken.IsCancellationRequested) 
    { 
        return await Task.FromCanceled(cancellationToken); 
    } 

    try 
    { 
        return await Task.FromResult(0); 
    } 
    catch (Exception exception) 
    { 
        return await Task.FromException(exception); 
    } 
} 

ConfigureAwait()

ConfigureAwait() peut être utilisé avec await pour indiquer quel est le contexte utilisé lors de la synchronisation de traitements asynchrones.

Contexte de synchronisation

Quand on parle de synchronisation dans le cadre de traitements asynchrones, on fait référence à la synchronisation qu’il peut être nécessaire d’effectuer entre les traitements pour accéder à une ou plusieurs ressources communes. Généralement ces ressources ne peuvent être sollicitées de façon concurrente par plusieurs traitements.

Par exemple, si on implémente une continuation qui doit être exécutée après l’exécution d’une task, il s’agit d’un cas particulier de synchronisation. En effet, la continuation ne pourra pas commencer son exécution avant que l’exécution de la task ne soit terminée.

Ainsi d’une façon générale, un contexte de synchronisation vise à indiquer tous les éléments nécessaires à la synchronisation entre plusieurs tasks. L’objet SynchronizationContext possède des éléments de contexte d’exécution sans les éléments relatifs à un thread:

  • La propriété statique SynchronizationContext.Current permet d’obtenir le contexte d’exécution du thread courant.
  • La méthode Post() permet de fournir une callback qui sera exécutée de façon asynchrone dans un contexte donné.
  • La méthode Send() permet de fournir une callback qui sera exécutée de façon synchrone dans un contexte donné.

Par exemple, dans le cas de WinForm, si on effectue un traitement asynchrone dont le résultat doit être affiché dans une TextBox, l’exécution se déroule de cette façon:

  • Lancement du traitement, par exemple, en cliquant sur un bouton dans le thread graphique (thread du control graphique permettant d’exécuter la boucle de message Win32).
  • Un traitement asynchrone est lancé dans un thread différent du thread de l’interface.
  • Quand le traitement asynchrone est terminé, le résultat doit être affecté dans la TextBox en utilisant le thread graphique et non dans le thread utilisé pour le traitement. Dans le cas contraire, on peut obtenir des erreurs du type:
    • “Cross-thread operation not valid. Control accessed from a thread other than the thread it was created on”.
    • “The calling thread cannot access this object because a different thread owns it”.
  • Pour mettre à jour le résultat dans la TextBox, il faut solliciter le thread graphique en y postant un nouveau traitement à effectuer, par exemple en exécutant:
    textBox.Invoke(new Action(() => { 
        textBox.Text = ... ; // Affectation du résultat 
    })); 
    

L’affectation du résultat dans la TextBox est donc considérée comme une continuation du traitement asynchrone. Si on utilise le contexte d’exécution à partir duquel on lance le traitement, une implémentation pourrait être:

var uiThreadContext = SynchronizationContext.Current; // Contexte du thread graphique 
Task.Run(() => { 

    // Traitement asynchrone 
    ... 
    // Affectation du résultat 

    uiThreadContext.Post(_ => { 
        textBox.Text = ... ; // Affectation du résultat en utilisant le contexte du thread graphique 
    }); 
}); 

Une implémentation différente permettrait de rendre la continuation plus évidente:

var uiThreadContext = SynchronizationContext.Current; 

Task workerTask = Task.Factory.StartNew(() => { 
    // Traitement asynchrone 
    ... 
}); 

workerTask.ContinueWith(() => { 
    uiThreadContext.Post(_ => { 
        textBox.Text = ... ;  
    }); 
}) 

TaskScheduler

L’objet SynchronizationContext est une abstraction pour permettre de lancer des traitements dans un contexte d’exécution donné. TaskScheduler est aussi une abstraction permettant de lancer des traitements sous la forme de Task. SynchronizationContext est général et TaskScheduler est plus spécifique aux tasks.

Certaines propriétés de TaskScheduler permettent de récupérer des instances particulières:

  • La propriété statique TaskScheduler.Current permet de retourner l’instance courante.
  • La propriété TaskScheduler.Default permet de retourner une instance permettant de s’interfacer avec le thread pool.
  • TaskScheduler.FromCurrentSynchronizationContext() permet de renvoyer une instance de TaskScheduler qui exécutera les tasks en utilisant SynchronizationContext.Current.

Même s’il n’est pas possible de paramètrer le TaskScheduler courant (la propriété TaskScheduler.Current ne permet pas de paramétrer une instance en entrée), il est possible de lancer une Task en indiquant un TaskScheduler particulier:

var customTaskScheduler = ... ; 
Task.Factory.StartNew(() => { ... }, default, TaskCreationOptions.None, customTaskScheduler); 

Un exemple équivalent au précédent permettrait d’affecter le résultat d’un traitement asynchrone en utilisant une continuation qui sera exécutée dans le thread graphique:

Task workerTask = Task.Factory.StartNew(() => { 
    // Traitement asynchrone 
    ... 
}); 

// La continuation est exécutée de le thread graphique: 
workerTask.ContinueWith(() => { 
    textBox.Text = ... ;  
}, TaskScheduler.FromCurrentSynchronizationContext()); 

await

La syntaxe async/await permet implicitement de gérer les cas d’une continuation. Dans le cas de l’exemple précédent, il suffirait d’écrire le code suivant pour que la continuation soit exécutée directement dans le thread de l’interface:

private async Task<TResult> AsyncProcess() { ... } 

var result = await AsyncProcess(); // Traitement asynchrone 
textBox.Text = result ; // Affectation du résultat directement dans le thead de l'interface. 

Ainsi ce qui se trouve après l’appel await est considéré comme une continuation. Cette continuation est exécutée avec le contexte de synchronisation courant (i.e. SynchronizationContext.Current) ou s’il est nul, le TaskScheduler courant (i.e. TaskScheduler.Current).

Implicitement la construction avec await capture le contexte d’exécution courant avant l’exécution de la tâche asynchrone et utilise ce contexte lors de l’exécution de la continuation.

ConfigureAwait(false)

ConfigureAwait() permet d’indiquer si on souhaite capturer le contexte d’exécution quand on utilise await:

private async Task<TResult> AsyncProcess() { ... } 

var result = await AsyncProcess().ConfigureAwait(...); 

ConfigureAwait() peut être utilisé avec les objets Task et ValueTask.

Par défaut, le contexte d’exécution est capturé, ainsi les syntaxes suivantes sont équivalentes:

var result = await AsyncProcess(); 

Et

var result = await AsyncProcess().ConfigureAwait(true); 

Pour éviter d’utiliser la contexte d’exécution d’origine lors de l’exécution de la continuation avec async/await, on peut utiliser ConfigureAwait(false). Il s’agit d’une petite optimisation pour simplifier le code généré par async/await de façon à indiquer au compilateur qu’il n’est pas nécessaire de capturer le contexte d’origine et que la continuation peut être exécutée dans un contexte différent du contexte d’origine.

Ce type d’optimisation ne peut être faite que lorsque l’utilisation du contexte d’origine n’est pas nécessaire (les mises à jour d’éléments graphiques sont exclues puisqu’elles doivent être exécutées dans le thread de l’interface).

L’intérêt principal de cette optimisation est d’améliorer sensiblement les performances puisqu’il n’est plus nécessaire d’exécuter le code des continuations dans le contexte d’origine. La dégradation en performance liée à l’ajout d’une tâche dans un autre contexte d’exécution n’est pas indispensable, la tâche pouvant, par exemple, être exécutée dans le même contexte que celui du traitement asynchrone.

Eviter des deadlocks
L’autre intérêt à utiliser ConfigurationAwait(false) est d’éviter un potentiel deadlock qui pourrait se produire dans certains cas d’utilisation d’async/await.

Certains contextes n’autorisent l’exécution que d’un seul thread à la fois: le thread graphique par exemple ou le contexte d’une requête ASP.NET MVC.

Par exemple, si on effectue un appel asynchrone avec await et qu’on attends le résultat de cet appel avec Wait(), Result ou GetAwaiter().GetResult():

private async Task<string> AsyncProcess() { ... }  

private async Task<string> GetResultFromAsyncProcess()  
{ 
    string result = await AsyncProcess(); 
    return $"Result is: {result}"; // Continuation exécutée dans le contexte d'origine 
} 

Task<string> processResult = GetResultFromAsyncProcess(); 
textBox.Text = processResult.Result; // Appel bloquant 

Dans le cas où ce traitement est effectué et qu’un seul thread ne peut être exécuté à la fois:

  1. La fonction GetResultFromAsyncProcess() est lancée dans le contexte d’origine.
  2. Le traitement de AsyncProcess() est lancé, toutefois il n’est pas terminé et la méthode renvoie une Task dont l’exécution n’est pas terminée.
  3. L’appel de .Result est bloquant et le thread du contexte d’origine est bloqué.
  4. La Task lancée par AsyncProcess() toutefois elle n’est pas terminée.
  5. La continuation après l’appel await est prête à être exécutée et attends que le contexte d’exécution soit disponible pour que la Task soit exécutée dans ce contexte.
  6. Un deadlock peut se produire car le thread du contexte d’origine est bloqué en attendant le résultat avec .Result et la continuation attends que le contexte soit disponible pour être exécuté.

Ce problème survient car le contexte d’exécution est le même entre l’appel d’origine et l’exécution de la continuation. Pour éviter le deadlock, une solution consiste à ne pas capturer le contexte d’origine lors de l’appel au traitement asynchrone. Les exécutions peuvent, ainsi, être exécutées dans des contextes différents:

private async Task<string> GetResultFromAsyncProcess()  
{ 
    string result = await AsyncProcess().ConfigureAwait(false); 
    return $"Result is: {result}";  
} 

ValueTask

C# 7.0 / .NET Core 2.0

L’inconvénient majeur à utiliser des fonctions renvoyant des instances des objets de type Task ou Task<T> est qu’elles nécessitent des allocations en mémoire. Dans le cas où des appels fréquents sont faits nécessitant le retour d’objets de type Task et sachant que Task est un objet de type référence, le coût en performance provoqué par les allocations dans le tas managé peut être significatif.

A partir de C# 7.0, l’objet ValueTask a été introduit de façon à éviter ces allocations dans le tas managé. ValueTask est un objet de type valeur et il est alloué sur la pile ce qui réduit le coût en performance à utiliser Task.

Pour utiliser l’objet ValueTask, il faut installer le package NuGet System.Threading.Tasks.Extensions dans le cas du framework .NET (ce n’est pas nécessaire avec .NET Core).

Compatibilité avec async/await

ValueTask est compatible avec async/await. Pour que la construction async/await soit possible, il faut que l’objet se trouvant après await possède la fonction:

public class AwaitableClass 
{ 
    public Awaiter GetAwaiter() { ... } 
} 

L’objet Awaiter doit satisfaire l’interface INotifyCompletion:

public class Awaiter: INotifyCompletion 
{ 
    public void GetResult() { ... } 
    public bool IsCompleted { get; } 
    public void OnCompleted(Action continuation) { ... } 
} 

C’est le cas pour l’objet ValueTask: ValueTask.GetAwaiter() renvoie un objet de type ValueTaskAwaiter qui satisfait INotifyCompletion. Symétriquement par rapport à Task.GetAwaiter() qui retourne TaskAwaiter.

Ainsi de la même façon que pour Task, il est possible d’utiliser une construction async/await avec ValueTask, par exemple:

public async ValueTask<int> GetResult() { ... } 

On peut effectuer des appels:

int result = await GetResult(); 

Il est possible d’utiliser ConfigureAwait() pour ne pas capturer le contexte d’exécution:

int result = await GetResult().ConfigureAwait(false); 

Il est ainsi possible d’utiliser des constructions impliquant simultanément les types Task/Task<T> ou ValueTask/ValueTask<T>:

public async ValueTask GetResultAsync(CancellationToken cancellationToken) 
{ 
    if (cancellationToken.IsCancellationRequested) 
    { 
        // Exécution annulée 
        return await Task.FromCanceled(cancellationToken); 
    } 

    try 
    { 
        // Retour immédiat d’un résultat 
        return await new ValueTask(0); 
    } 
    catch (Exception exception) 
    { 
        // Retour avec une exception 
        return await Task.FromException(exception); 
    } 

} 

ValueTask ne convient pas à tous les usages

ValueTask ne remplace pas les objets Task. ValueTask est plus approprié dans certains cas d’utilisation précis mais il ne couvre pas tous les usages de Task. Comme on l’a indiqué, le but de ValueTask est d’éviter d’allouer beaucoup d’objets dans le tas managé dans le cas où des appels à une méthode effectuant des tâches asynchrones seraient fréquents. Toutefois ValueTask est un objet de type valeur qui, dans certains cas, peut s’avérer moins performant à manipuler qu’un objet de type référence.

Par exemple, si le type T de retour dans une fonction renvoyant ValueTask est particulièrement volumineux, chaque retour de fonction va occasionner une copie par valeur de l’objet ValueTask (car ValueTask est un objet de type valeur). Cette copie peut être moins performante que si on utilisait Task. Il faut donc comparer les performances d’exécution en utilisant ValueTask et Task pour être sûr d’utiliser l’objet le plus approprié.

Cas synchrone

Le cas d’utilisation le plus simple est le cas synchrone c’est-à-dire que l’asynchronisme n’est pas nécessaire et un résultat peut être renvoyé dans l’immédiat. Ce cas de figure est semblable à l’utilisation de Task.FromResult(). On peut utiliser directement le constructeur:

T result = ...; 
return new ValueTask(result); 

Dans les cas plus rares d’une exception ou d’une tâche annulée, il est possible de créer un objet ValueTask à partir d’une Task:

new ValueTask(Task.FromCancelled(cancellationToken)); 

ou

new ValueTask(Task.FromException(exception)); 

Cas asynchrone

Dans le cas asynchrone, plusieurs cas d’utilisation ne sont pas possibles ou proscrits avec ValueTask. Ainsi d’une façon générale, un objet ValueTask:

  • Ne peut être appelé qu’une seule fois avec await.
  • Ne peut pas être appelé de façon concurrente.
  • Utiliser ValueTask.GetAwaiter().GetResult() n’est pas bloquant contrairement à Task. Utiliser ValueTask.GetAwaiter().GetResult() dans le cas où l’exécution n’est pas terminé peut conduire à des comportements imprévus.

Dans le cas où on est confronté à ces cas de figure, il faut:

  • Privilégier l’utilisation des objets Task
  • Utiliser la fonction ValueTask.AsTask() pour extraire un objet Task de la ValueTask.

Tous ces cas d’utilisation peuvent réduire grandement l’intérêt ValueTask.

Pour éviter les mauvaises utilisations des objets ValueTask en retour de fonction, il est préférable de l’utiliser directement avec await sans stocker le retour d’une fonction async dans une variable, par exemple si on considère la fonction:

public ValueTask GetAsyncResult() 
{ 
    // ... 
} 

Il faut privilégier les utilisations avec await:

int result = await GetAsyncResult(); 

Stocker le retour dans une variable peut inciter à implémenter des cas d’utilisations à proscrire avec ValueTask, par exemple:

ValueTask resultValueTask = GetAsyncResult(); 

// Plusieurs appels avec await: 
int result1 = await resultValueTask; 
int result2 = await resultValueTask; // A EVITER 
 
// Appels concurrents 
Task.Run(async () => await resultValueTask); 
Task.Run(async () => await resultValueTask); // A EVITER 

// Utiliser GetAwaiter().GetResult() sur une exécution non terminée 
int result = resultValueTask.GetAwaiter().GetResult(); // A EVITER 

Dans le cas asynchrone, toutes ces restrictions et le fait que ValueTask est manipulé le plus souvent par copie (étant un objet de type valeur), peuvent rendre ce type d’objet moins performant que Task. Ainsi on pourrait de demander l’intérêt à utiliser ValueTask dans le cas asynchrone. Cet intérêt se trouve principalement dans la possibilité d’utiliser des objets satisfaisant IValueTaskSource.

IValueTaskSource

C# 7.0 / .NET Core 2.1

L’intérêt principal de ValueTask est de pouvoir l’utiliser avec des objets satisfaisant l’interface IValueTaskSource. Cette possibilité est apparue à partir de .NET Core 2.1 (dans le cas du framework .NET, il suffit d’utiliser le package System.Threading.Tasks.Extensions (comme indiqué plus haut).

La prise en charge de IValueTaskSource par le constructeur de ValueTask n’est pas, à proprement parlé, une innovation de C# 7. Cette évolution est apportée par .NET Core 2.1 ou par l’utilisation du package System.ThreadingTask.Extensions (qui est compatible à partir du framework .NET 4.5).

Il est ainsi possible de construire une instance de ValueTask en utilisant le constructeur:

IValueTaskSource valueTaskSource = ...; 
int token = ...; 
ValueTask valueTask = new ValueTask(valueTaskSource, token); 

IValueTaskSource permet d’ajouter une abstraction pour permettre de gérer l’exécution d’une tâche asynchrone en séparant le comportement de la tâche avec l’obtiention de son résultat. IValueTaskSource se présente de cette façon:

public interface IValueTaskSource 
{ 
    T GetResult(short token); 
    ValueTaskSourceStatus GetStatus(short token); 
    void OnCompleted(Action) continuation, objet state, short token, ValueTaskSourceOnCompletedFlags flags); 
} 

avec:

  • L’argument token est utilisé pour identifier le traitement dans le cas où plusieurs traitements sont effectués de façon concurrente.
  • T GetResult(short token): la fonction permettant d’obtenir le résultat du traitement (le token permet d’identifier le traitement).
  • ValueTaskSourceStatus GetStatus(short token): renvoie le statut d’exécution d’un traitement (identifié grâce au token). Les statuts possibles sont Pending, Succeeded, Faulted ou Canceled.
  • void OnCompleted(Action) continuation, objet state, short token, ValueTaskSourceOnCompletedFlags flags) permet d’exécuter une continuation faisait suite à l’exécution d’un traitement identifié par un token. Cette fonction ne doit qu’exécuter la continuation suivant le résultat du traitement.

Pour utiliser ValueTask dans le cas asynchrone, il faut donc implémenter une classe satisfaisant IValueTaskSource. L’intérêt est d’utiliser une seule instance d’un objet IValueTaskSource pour exécuter une série de traitements asynchrones. Le résultat est renvoyé sous la forme ValueTask après avoir instancié un objet de ce type avec le constructeur permettant d’utiliser une instance IValueTaskSource. Tout ce mécanisme permet de diminuer le nombre d’allocations effectuées dans le tas managé (puisque IValueTaskSource est instancié une seule fois et ValueTask est un objet de type valeur instancié sur la pile).

On peut, ainsi, renvoyer plusieurs fois un résultat en utilisant async/await pour des traitements effectués de façon concurrente:

public ValueTask RunAsync() 
{ 
    // ... 
    return new ValueTask(this.valueTaskSource, token); 
} 

T result = await RunAsync(); 

On remarque qu’une nouvelle instance de ValueTask est créée même si plusieurs appels sont faits à RunAsync(). Ainsi même si il n’est pas conseillé d’effectuer plusieurs appels ou des appels concurrents à une même instance de ValueTask, il est possible de créer une nouvelle instance pour chaque utilisation. Comme ValueTask est alloué sur la pile et non dans le tas managé, le Garbage Collector n’est pas sollicité dans le cas où des appels sont effectués de façon très répétitive.

Ainsi l’objet satisfaisant IValueTaskSource est instancié une fois et ajouté au pool de tâche. A chaque exécution asynchrone d’un traitement dans cet objet, un objet ValueTask est créé et retourné en utilisant await. Ainsi chaque exécution du traitement rajoute un objet ValueTask au pool correspondant à un traitement asynchrone. Le paramètre token fourni en paramètre du constructeur de ValueTask permet de différencier chaque exécution d’un traitement qui sera rajouté au pool (chacun de ces traitements créant une instance différente de ValueTask):

public class AwaitableRecurringAsyncProcess : IValueTaskSource 
{ 
    public ValueTask LaunchFirstProcess() { ... } 

    public ValueTask LaunchSecondProcess() { ... } 
 
    // ... 
} 

L’implémentation d’un objet satisfaisant IValueTaskSource n’est pas triviale et expose à une complexité en terme de traitement parallèle. Le but de ValueTask dans ce cadre n’est pas d’éviter cette complexité. En revanche, le pattern async/await associé à ValueTask permet d’apporter une solution technique pour éviter d’instancier trop d’objets sur le tas managé dans le cas où les traitements sont répétitifs et doivent être exécuté de façon optimale.

Plusieurs implémentations d’objets satisfaisant IValueTaskSource ont été faite pour .NET Core 2.1 dans le cadre de communication par socket avec AwaitableSocketAsyncEventArgs:

internal sealed class AwaitableSocketAsyncEventArgs: SocketAsyncEventArgs, IValueTaskSource, IValueTaskSource<int> 
{ 
    public ValueTask<int> ReceiveAsync(Socket socket, CancellationToken cancellationToken) 
    { ... } 

    public ValueTask<int> SendAsync(Socket socket, CancellationToken cancellationToken) 
    { ... } 
} 

Dans cet exemple, tant que la socket n’est pas fermée et que IValueTaskSource.GetStatus() ne retourne pas ValueTaskSourceStatus.Succeeded, plusieurs appels peuvent être faits pour envoyer et recevoir des paquets de façon asynchrone en utilisant SendAsync() et ReceiveAsync().

D’autres cas d’utilisation peuvent servir d’exemples pour implémenter un objet satisfaisant IValueTaskSource:

Leave a Reply