Comment créer une méthode asynchrone en C #?

196

Chaque article de blog que j'ai lu vous indique comment utiliser une méthode asynchrone en C #, mais pour une raison étrange, n'expliquez jamais comment créer vos propres méthodes asynchrones à consommer. J'ai donc ce code en ce moment qui consomme ma méthode:

private async void button1_Click(object sender, EventArgs e)
{
    var now = await CountToAsync(1000);
    label1.Text = now.ToString();
}

Et j'ai écrit cette méthode qui est CountToAsync:

private Task<DateTime> CountToAsync(int num = 1000)
{
    return Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < num; i++)
        {
            Console.WriteLine("#{0}", i);
        }
    }).ContinueWith(x => DateTime.Now);
}

Est-ce, l'utilisation de Task.Factory, la meilleure façon d'écrire une méthode asynchrone, ou devrais-je l'écrire d'une autre manière?

Khalid Abuhakmeh
la source
22
Je pose une question générale sur la manière de structurer une méthode. Je veux juste savoir par où commencer pour transformer mes méthodes déjà synchrones en méthodes asynchrones.
Khalid Abuhakmeh
2
OK, alors qu'est- ce une méthode typique synchrone faire , et pourquoi souhaitez - vous faire asynchrone ?
Eric Lippert
Disons que je dois traiter par lots un tas de fichiers et renvoyer un objet de résultat.
Khalid Abuhakmeh
1
OK, alors: (1) qu'est-ce que l'opération à latence élevée: obtenir les fichiers - parce que le réseau pourrait être lent, ou quoi que ce soit - ou faire le traitement - parce que cela consomme beaucoup de CPU, par exemple. Et (2) vous n'avez toujours pas dit pourquoi vous voulez qu'il soit asynchrone en premier lieu. Y a-t-il un thread d'interface utilisateur que vous ne souhaitez pas bloquer, ou quoi?
Eric Lippert
21
@EricLippert L'exemple donné par l'op est très basique, il n'a vraiment pas besoin d'être aussi compliqué.
David B.

Réponses:

227

Je ne recommande pas StartNewsauf si vous avez besoin de ce niveau de complexité.

Si votre méthode async dépend d'autres méthodes async, l'approche la plus simple consiste à utiliser le asyncmot - clé:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  for (int i = 0; i < num; i++)
  {
    await Task.Delay(TimeSpan.FromSeconds(1));
  }

  return DateTime.Now;
}

Si votre méthode asynchrone effectue un travail sur le processeur, vous devez utiliser Task.Run:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  await Task.Run(() => ...);
  return DateTime.Now;
}

Vous trouverez peut-être mon async/ awaitintro utile.

Stephen Cleary
la source
10
@Stephen: "Si votre méthode asynchrone dépend d'autres méthodes asynchrones" - ok, mais que faire si ce n'est pas le cas. Et si vous essayiez d'encapsuler du code qui appelle BeginInvoke avec du code de rappel?
Ricibob
1
@Ricibob: Vous devriez utiliser TaskFactory.FromAsyncpour envelopper BeginInvoke. Je ne sais pas ce que vous entendez par "code de rappel"; n'hésitez pas à poster votre propre question avec du code.
Stephen Cleary
@Stephen: Merci - oui TaskFactory.FromAsync est ce que je cherchais.
Ricibob
1
Stephen, dans la boucle for, la prochaine itération est-elle appelée immédiatement ou après les Task.Delayretours?
jp2code
3
@ jp2code: awaitest une "attente asynchrone", donc il ne passe à l'itération suivante qu'après la fin de la tâche retournée par Task.Delay.
Stephen Cleary
12

Si vous ne vouliez pas utiliser async / await dans votre méthode, mais que vous la "décorez" de manière à pouvoir utiliser le mot-clé await de l'extérieur, TaskCompletionSource.cs :

public static Task<T> RunAsync<T>(Func<T> function)
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ =>          
    { 
        try 
        {  
           T result = function(); 
           tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
   }); 
   return tcs.Task; 
}

D'ici et d' ici

Pour prendre en charge un tel paradigme avec les tâches, nous avons besoin d'un moyen de conserver la façade de la tâche et de la possibilité de faire référence à une opération asynchrone arbitraire en tant que tâche, mais de contrôler la durée de vie de cette tâche selon les règles de l'infrastructure sous-jacente qui fournit le asynchrone, et de le faire d'une manière qui ne coûte pas beaucoup. C'est le but de TaskCompletionSource.

J'ai vu est également utilisé dans la source .NET, par exemple. WebClient.cs :

    [HostProtection(ExternalThreading = true)]
    [ComVisible(false)]
    public Task<string> UploadStringTaskAsync(Uri address, string method, string data)
    {
        // Create the task to be returned
        var tcs = new TaskCompletionSource<string>(address);

        // Setup the callback event handler
        UploadStringCompletedEventHandler handler = null;
        handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.UploadStringCompleted -= completion);
        this.UploadStringCompleted += handler;

        // Start the async operation.
        try { this.UploadStringAsync(address, method, data, tcs); }
        catch
        {
            this.UploadStringCompleted -= handler;
            throw;
        }

        // Return the task that represents the async operation
        return tcs.Task;
    }

Enfin, j'ai trouvé utile aussi ce qui suit:

On me pose cette question tout le temps. L'implication est qu'il doit y avoir un thread quelque part qui bloque l'appel d'E / S à la ressource externe. Ainsi, le code asynchrone libère le thread de requête, mais uniquement au détriment d'un autre thread ailleurs dans le système, non? Non pas du tout. Pour comprendre pourquoi les demandes asynchrones évoluent, je vais tracer un exemple (simplifié) d'un appel d'E / S asynchrone. Disons qu'une requête doit écrire dans un fichier. Le thread de demande appelle la méthode d'écriture asynchrone. WriteAsync est implémenté par la bibliothèque de classes de base (BCL) et utilise des ports d'achèvement pour ses E / S asynchrones. Ainsi, l'appel WriteAsync est transmis au système d'exploitation en tant qu'écriture de fichier asynchrone. Le système d'exploitation communique ensuite avec la pile de pilotes, transmettant les données à écrire dans un paquet de demande d'E / S (IRP). C'est là que les choses deviennent intéressantes: Si un pilote de périphérique ne peut pas gérer un IRP immédiatement, il doit le gérer de manière asynchrone. Ainsi, le pilote dit au disque de commencer à écrire et renvoie une réponse «en attente» au système d'exploitation. Le système d'exploitation transmet cette réponse «en attente» à la BCL, et la BCL renvoie une tâche incomplète au code de gestion des demandes. Le code de gestion des demandes attend la tâche, qui renvoie une tâche incomplète de cette méthode et ainsi de suite. Enfin, le code de gestion des demandes finit par renvoyer une tâche incomplète à ASP.NET, et le thread de demande est libéré pour revenir au pool de threads. Le code de gestion des demandes attend la tâche, qui renvoie une tâche incomplète de cette méthode et ainsi de suite. Enfin, le code de gestion des demandes finit par renvoyer une tâche incomplète à ASP.NET, et le thread de demande est libéré pour revenir au pool de threads. Le code de gestion des demandes attend la tâche, qui renvoie une tâche incomplète de cette méthode et ainsi de suite. Enfin, le code de gestion des demandes finit par renvoyer une tâche incomplète à ASP.NET, et le thread de demande est libéré pour revenir au pool de threads.

Introduction à Async / Await sur ASP.NET

Si l'objectif est d'améliorer l'évolutivité (plutôt que la réactivité), tout repose sur l'existence d'une E / S externe qui offre la possibilité de le faire.

Alberto
la source
1
Si vous voyez le commentaire au-dessus de l' implémentation de Task.Run, par exemple, met en file d' attente le travail spécifié à exécuter sur le ThreadPool et renvoie un descripteur de tâche pour ce travail . Vous faites la même chose. Je suis sûr que Task.Run sera meilleur, car il gère en interne CancelationToken et fait quelques optimisations avec les options de planification et d'exécution.
unsafePtr
alors ce que vous avez fait avec ThreadPool.QueueUserWorkItem pourrait être fait avec Task.Run, non?
Razvan le
-1

Un moyen très simple de rendre une méthode asynchrone consiste à utiliser la méthode Task.Yield (). Comme l'indique MSDN:

Vous pouvez utiliser await Task.Yield (); dans une méthode asynchrone pour forcer la méthode à se terminer de manière asynchrone.

Insérez-le au début de votre méthode et il reviendra alors immédiatement à l'appelant et terminera le reste de la méthode sur un autre thread.

private async Task<DateTime> CountToAsync(int num = 1000)
{
    await Task.Yield();
    for (int i = 0; i < num; i++)
    {
        Console.WriteLine("#{0}", i);
    }
    return DateTime.Now;
}
SalgoMato
la source
Dans une application WinForms, le reste de la méthode se terminera dans le même thread (le thread d'interface utilisateur). Le Task.Yield()est en effet utile, pour les cas où vous souhaitez vous assurer que le rendu Taskne sera pas immédiatement terminé lors de la création.
Theodor Zoulias le