web-dev-qa-db-fra.com

Écrivez votre propre méthode asynchrone

Je voudrais savoir comment écrire vos propres méthodes asynchrones de la manière "correcte".

J'ai vu beaucoup de messages expliquant le modèle asynchrone/attente comme ceci:

http://msdn.Microsoft.com/en-us/library/hh191443.aspx

// Three things to note in the signature: 
//  - The method has an async modifier.  
//  - The return type is Task or Task<T>. (See "Return Types" section.)
//    Here, it is Task<int> because the return statement returns an integer. 
//  - The method name ends in "Async."
async Task<int> AccessTheWebAsync()
{ 
    // You need to add a reference to System.Net.Http to declare client.
    HttpClient client = new HttpClient();

    // GetStringAsync returns a Task<string>. That means that when you await the 
    // task you'll get a string (urlContents).
    Task<string> getStringTask = client.GetStringAsync("http://msdn.Microsoft.com");

    // You can do work here that doesn't rely on the string from GetStringAsync.
    DoIndependentWork();

    // The await operator suspends AccessTheWebAsync. 
    //  - AccessTheWebAsync can't continue until getStringTask is complete. 
    //  - Meanwhile, control returns to the caller of AccessTheWebAsync. 
    //  - Control resumes here when getStringTask is complete.  
    //  - The await operator then retrieves the string result from getStringTask. 
    string urlContents = await getStringTask;

    // The return statement specifies an integer result. 
    // Any methods that are awaiting AccessTheWebAsync retrieve the length value. 
    return urlContents.Length;
}

private void DoIndependentWork()
{
    resultsTextBox.Text += "Working........\r\n";
}

Cela fonctionne très bien pour toute méthode .NET qui implémente déjà cette fonctionnalité comme

  • Opérations System.IO
  • Opérations DataBase
  • Opérations liées au réseau (téléchargement, téléchargement ...)

Mais que se passe-t-il si je veux écrire ma propre méthode qui prend un certain temps pour terminer là où il n'y a tout simplement pas de méthode que je peux utiliser et la lourde charge est dans la méthode DoIndependentWork de l'exemple ci-dessus?

Dans cette méthode, je pourrais faire:

  • Manipulations de cordes
  • Calculs
  • Manipuler mes propres objets
  • Agréger, comparer, filtrer, regrouper, gérer des trucs
  • Liste des opérations, ajout, suppression, adaptation

Encore une fois, je suis tombé sur de nombreux messages où les gens font simplement ce qui suit (en reprenant l'exemple ci-dessus):

async Task<int> AccessTheWebAsync()
{ 
    HttpClient client = new HttpClient();

    Task<string> getStringTask = client.GetStringAsync("http://msdn.Microsoft.com");

    await DoIndependentWork();

    string urlContents = await getStringTask;

    return urlContents.Length;
}

private Task DoIndependentWork()
{
    return Task.Run(() => {

        //String manipulations
        //Calculations
        //Handling my own objects
        //Aggregating, comparing, filtering, grouping, handling stuff
        //List operations, adding, removing, coping
    });
}

Vous pouvez remarquer que les changements sont que DoIndependentWork renvoie maintenant une tâche et dans la tâche AccessTheWebAsync la méthode a obtenu un await.

Les opérations de charges lourdes sont désormais capsulées à l'intérieur d'une Task.Run(), est-ce tout ce qu'il faut? Si c'est tout ce qu'il faut, c'est la seule chose que je dois faire pour fournir la méthode asynchrone pour chaque méthode de ma bibliothèque:

public class FooMagic
{
    public void DoSomeMagic()
    {
        //Do some synchron magic...
    }

    public Task DoSomeMagicAsync()
    {
        //Do some async magic... ?!?
        return Task.Run(() => { DoSomeMagic(); });
    }
}

Ce serait bien si vous pouviez me l'expliquer car même une question votée comme celle-ci: Comment écrire une méthode asynchrone simple? l'explique uniquement avec des méthodes déjà existantes et en utilisant simplement le modèle asyn/wait comme ce commentaire de la question mentionnée l'amène au point: Comment écrire une méthode asynchrone simple?

40
Rand Random

Réponse réelle

Vous faites cela en utilisant TaskCompletionSource , qui a un Promise Task qui n'exécute aucun code et seulement:

"Représente le côté producteur d'une tâche non liée à un délégué, donnant accès au côté consommateur via la propriété Task."

Vous renvoyez cette tâche à l'appelant lorsque vous démarrez l'opération asynchrone et vous définissez le résultat (ou exception/annulation) lorsque vous y mettez fin. Assurez-vous que l'opération est vraiment asynchrone.

Voici un bon exemple de ce type de méthode racine de tous les asynchrones dans l'implémentation de AsyncManualResetEvent de Stephen Toub:

class AsyncManualResetEvent 
{ 
    private volatile TaskCompletionSource<bool> _tcs = new TaskCompletionSource<bool>();

    public Task WaitAsync() { return _tcs.Task; } 
    public void Set() { _tcs.TrySetResult(true); } 
    public void Reset() 
    { 
        while (true) 
        { 
            var tcs = _tcs; 
            if (!tcs.Task.IsCompleted || 
                Interlocked.CompareExchange(ref _tcs, new TaskCompletionSource<bool>(), tcs) == tcs) 
                return; 
        } 
    } 
}

Contexte

Il y a essentiellement deux raisons d'utiliser async-await:

  1. Évolutivité améliorée : Lorsque vous avez I/O travail intensif (ou autres opérations intrinsèquement asynchrones), vous pouvez l'appeler de manière asynchrone et ainsi vous libérez le thread appelant et il est capable de faire d'autres travaux dans l'intervalle.
  2. Déchargement: Lorsque vous avez un travail intensif CPU, vous pouvez l'appeler de manière asynchrone, ce qui déplace le travail d'un thread à un autre (principalement utilisé pour les fils GUI).

Ainsi, la plupart des appels asynchrones du framework .Net prennent en charge async dès le départ et pour le déchargement, vous utilisez Task.Run (comme dans votre exemple). Le seul cas où vous avez réellement besoin d'implémenter async vous-même est lorsque vous créez un nouvel appel asynchrone (I/O ou async constructions de synchronisation par exemple).

Ces cas sont extrêmement rares, c'est pourquoi vous trouvez principalement des réponses

"L'explique uniquement avec les méthodes déjà existantes et en utilisant simplement async/await modèle"


Vous pouvez aller plus loin dans The Nature of TaskCompletionSource

24
i3arnon

Ce serait bien si vous pouviez m'expliquer: comment écrire une méthode asynchrone simple?

Tout d'abord, nous devons comprendre ce que signifie une méthode async. Quand on expose une méthode async à l'utilisateur final consommant la méthode async, vous lui dites: "Écoutez, cette méthode vous reviendra rapidement avec un promesse de terminer dans un avenir proche ". C'est ce que vous garantissez à vos utilisateurs.

Maintenant, nous devons comprendre comment Task rend cette "promesse" possible. Comme vous le demandez dans votre question, pourquoi simplement ajouter un Task.Run à l'intérieur de ma méthode permet d'être attendu en utilisant le mot clé await?

Un Task implémente le modèle GetAwaiter, ce qui signifie qu'il renvoie un objet appelé awaiter (il est en fait appelé TaskAwaiter ). L'objet TaskAwaiter implémente les interfaces INotifyCompletion ou ICriticalNotifyCompletion , exposant une méthode OnCompleted.

Tous ces goodies sont à leur tour utilisés par le compilateur une fois que le mot clé await est utilisé. Le compilateur s'assurera qu'au moment de la conception, votre objet implémente GetAwaiter, et à son tour l'utiliser pour compiler le code dans une machine à états, ce qui permettra à votre programme de redonner le contrôle à l'appelant une fois attendu, et reprendre lorsque ces travaux seront terminés.

Maintenant, il y a quelques directives à suivre. Une véritable méthode asynchrone n'utilise pas de threads supplémentaires en arrière-plan pour faire son travail (Stephan Cleary explique cela à merveille dans Il n'y a pas de thread ), ce qui signifie que l'exposition d'un méthode qui utilise Task.Run inside est un peu trompeur pour les consommateurs de votre API, car ils n'assumeront aucun filetage supplémentaire impliqué dans votre tâche. Ce que vous devez faire est d'exposer votre API de manière synchrone et de laisser l'utilisateur le décharger à l'aide de Task.Run lui-même, contrôlant le flux d'exécution.

async Les méthodes sont principalement utilisées pour les opérations liées aux E/S , car elles n'ont naturellement pas besoin de threads pour être consommées alors que le = IO opération est en cours d'exécution, et c'est pourquoi nous les voyons beaucoup dans les classes chargées de faire IO opérations, telles que les appels de disque dur, les appels réseau, etc.).

Je suggère de lire l'article des équipes Parallel PFX Dois-je exposer les wrappers asynchrones pour les méthodes synchrones? qui parle exactement de ce que vous essayez de faire et pourquoi il n'est pas recommandé.

24
Yuval Itzchakov

TL; DR:

Task.Run() est ce que vous voulez, mais faites attention à le cacher dans votre bibliothèque.

Je peux me tromper, mais vous cherchez peut-être des conseils sur le fonctionnement asynchrone du code lié au CPU [par Stephen Cleary]. J'ai eu du mal à trouver cela aussi, et je pense que la raison pour laquelle c'est si difficile est que ce n'est pas ce que vous êtes censé faire pour une bibliothèque - un peu ...

Article

L'article lié est une bonne lecture (5-15 minutes, selon) qui donne une quantité décente de détails sur les manières et les raisons d'utiliser Task.Run() dans le cadre d'une API par rapport à son utilisation pour ne pas bloquer une interface utilisateur thread - et distingue deux "types" de processus de longue durée que les gens aiment exécuter de manière asynchrone:

  • Processus lié au CPU (un processus qui fonctionne/fonctionne réellement et a besoin d'un thread séparé pour faire son travail)
  • Fonctionnement vraiment asynchrone (parfois appelé lié aux E/S - qui fait quelques choses ici et là avec un tas de temps d'attente entre les actions et serait mieux vaut ne pas monopoliser un fil pendant qu'il est assis sans rien faire).

L'article aborde l'utilisation des fonctions API dans divers contextes et explique si les architectures associées "préfèrent" les méthodes de synchronisation ou async et comment une API avec des signatures de méthode de synchronisation et async "ressemble" à un développeur.

Répondre

La dernière section " OK, assez sur les mauvaises solutions? Comment pouvons-nous résoudre ce problème de la bonne façon ??? " va dans ce que je pense que vous êtes poser des questions sur, se terminant par ceci:

Conclusion: n'utilisez pas Task.Run dans la mise en œuvre de la méthode; utilisez plutôt Task.Run pour appeler la méthode.

Fondamentalement, Task.Run() 'monopolise' un thread, et est donc la chose à utiliser pour le travail lié au CPU, mais cela revient à il est utilisé. Lorsque vous essayez de faire quelque chose qui nécessite beaucoup de travail et que vous ne voulez pas bloquer le thread d'interface utilisateur, utilisez Task.Run() pour exécuter la fonction de travail acharné directement = (c'est-à-dire dans le gestionnaire d'événements ou votre code basé sur l'interface utilisateur):

class MyService
{
  public int CalculateMandelbrot()
  {
    // Tons of work to do in here!
    for (int i = 0; i != 10000000; ++i)
      ;
    return 42;
  }
}

...

private async void MyButton_Click(object sender, EventArgs e)
{
  await Task.Run(() => myService.CalculateMandelbrot());
}

Mais ... ne le faites pas cachez votre Task.Run() dans une fonction API suffixée -Async Si c'est une fonction liée au CPU, comme fondamentalement tous les -Async Est vraiment asynchrone et non liée au processeur.

// Warning: bad code!
class MyService
{
  public int CalculateMandelbrot()
  {
    // Tons of work to do in here!
    for (int i = 0; i != 10000000; ++i)
      ;
    return 42;
  }

  public Task<int> CalculateMandelbrotAsync()
  {
    return Task.Run(() => CalculateMandelbrot());
  }
}

En d'autres termes, n'appelez pas une fonction liée au CPU -Async, Car les utilisateurs supposeront qu'elle est liée aux IO - appelez-la simplement de manière asynchrone à l'aide de Task.Run(), et laissez les autres utilisateurs faire de même quand ils le jugent approprié. Alternativement, nommez-le quelque chose d'autre qui a du sens pour vous (peut-être BeginAsyncIndependentWork() ou StartIndependentWorkTask()).

13
Code Jockey