web-dev-qa-db-fra.com

Est-il vrai qu'async ne doit pas être utilisé pour des tâches à processeur élevé?

Je me demandais s'il était vrai que async-await ne devrait pas être utilisé pour des tâches "à processeur élevé". J'ai vu cela revendiqué dans une présentation.

Donc, je suppose que cela voudrait dire quelque chose comme

Task<int> calculateMillionthPrimeNumber = CalculateMillionthPrimeNumberAsync();
DoIndependentWork();
int p = await calculateMillionthPrimeNumber;

Ma question est est-ce que ce qui précède peut être justifié, ou sinon, existe-t-il un autre exemple de création asynchrone d'une tâche à processeur élevé?

22
user7127000

Je me demandais s'il était vrai que l'attente asynchrone ne devrait pas être utilisée pour des tâches "à processeur élevé".

Oui c'est vrai.

Ma question est: est-ce que ce qui précède pourrait être justifié?

Je dirais que ce n'est pas justifié. Dans le cas général, évitez d'utiliser Task.Run pour implémenter des méthodes avec des signatures asynchrones. N'exposez pas les wrappers asynchrones pour les méthodes synchrones . Cela évite toute confusion chez les consommateurs, en particulier sur ASP.NET.

Cependant, l'utilisation de Task.Run dans call est synonyme de méthode synchrone, par exemple dans une application d'interface utilisateur. De cette manière, vous pouvez utiliser le multithreading (Task.Run) pour garder le fil de l'interface utilisateur libre et le consommer avec élégance avec await:

var task = Task.Run(() => CalculateMillionthPrimeNumber());
DoIndependentWork();
var prime = await task;
7
Stephen Cleary

Il existe en fait deux utilisations principales de async/wait. L'une (et j'ai cru comprendre que c'est l'une des principales raisons pour lesquelles elle a été intégrée dans la structure) consiste à permettre au fil d'appel d'effectuer d'autres tâches en attendant un résultat. C’est principalement pour les tâches liées aux E/S (c'est-à-dire les tâches pour lesquelles le "blocage" principal est une sorte d'E/S - en attente d'un disque dur, d'un serveur, d'une imprimante, etc. pour répondre ou terminer sa tâche).

En remarque, si vous utilisez async/wait de cette manière, il est important de vous assurer que vous l'avez implémenté de manière à ce que le fil d'appel puisse réellement effectuer un autre travail en attendant le résultat. J'ai vu beaucoup de cas où des gens font des choses comme "A attend B, qui attend C"; cela peut ne pas donner de meilleurs résultats que si A appelait simplement B de manière synchrone et B appelait simplement C de manière synchrone (car le thread appelant n'a jamais été autorisé à effectuer d'autres tâches pendant qu'il attend les résultats de B et C).

Dans le cas de tâches liées aux E/S, il est inutile de créer un thread supplémentaire juste pour attendre un résultat. Mon analogie habituelle ici est de penser à commander dans un restaurant avec 10 personnes dans un groupe. Si la première personne à qui le serveur demande de passer commande n'est pas encore prête, le serveur n'attend pas qu'il soit prêt avant de prendre la commande de quelqu'un d'autre, il ne fait pas venir un deuxième serveur juste pour attendre le premier. La meilleure chose à faire dans ce cas est de demander les 9 autres membres du groupe pour leurs ordres; j'espère que, au moment où ils auront commandé, le premier type sera prêt. Sinon, au moins, le serveur gagne encore du temps, car il passe moins de temps en veille.

Il est également possible d'utiliser des éléments tels que Task.Run pour effectuer des tâches liées au processeur (et c'est la deuxième utilisation pour cela). Pour suivre notre analogie ci-dessus, c’est un cas où il serait généralement utile d’avoir plus de serveurs - par exemple. s’il y avait trop de tables pour un seul serveur à desservir. Vraiment, tout ce que cela fait réellement "en coulisse" est d'utiliser le pool de threads; C’est l’une des constructions possibles pour effectuer un travail lié au processeur (par exemple, le placer "directement" sur le pool de threads, créer explicitement un nouveau thread ou utiliser un Background Worker ) pour que le mécanisme à mettre fin soit une question de conception. en utilisant.

L’un des avantages de async/await ici est qu’il peut (dans certaines circonstances) réduire la quantité de logique de verrouillage/synchronisation explicite que vous devez écrire manuellement. Voici une sorte d'exemple stupide:

private static async Task SomeCPUBoundTask()
    {
        // Insert actual CPU-bound task here using Task.Run
        await Task.Delay(100);
    }

    public static async Task QueueCPUBoundTasks()
    {
        List<Task> tasks = new List<Task>();

        // Queue up however many CPU-bound tasks you want
        for (int i = 0; i < 10; i++)
        {
            // We could just call Task.Run(...) directly here
            Task task = SomeCPUBoundTask();

            tasks.Add(task);
        }

        // Wait for all of them to complete
        // Note that I don't have to write any explicit locking logic here,
        // I just tell the framework to wait for all of them to complete
        await Task.WhenAll(tasks);
    }

Évidemment, je suppose ici que les tâches sont complètement parallélisables. Notez également que vous pourriez auriez utilisé le pool de threads ici, mais ce serait un peu moins pratique car vous auriez besoin d’un moyen de déterminer vous-même si tous avaient terminé (plutôt que simplement). laisser le cadre comprendre cela pour vous). Vous avez peut-être également pu utiliser une boucle Parallel.For ici.

8
EJoshuaS

Supposons que votre CalculateMillionthPrimeNumber ressemble à ce qui suit (pas très efficace ou idéal dans son utilisation de goto mais très simple à comprendre):

public int CalculateMillionthPrimeNumber()
{
    List<int> primes = new List<int>(1000000){2};
    int num = 3;
    while(primes.Count < 1000000)
    {
        foreach(int div in primes)
        {
            if ((num / div) * div == num)
                goto next;
        }
        primes.Add(num);
        next:
            ++num;
    }
    return primes.Last();
}

Or, il n’est pas utile ici que cela puisse faire quelque chose de manière asynchrone. Faisons-en une méthode de retour de tâche utilisant async:

public async Task<int> CalculateMillionthPrimeNumberAsync()
{
    List<int> primes = new List<int>(1000000){2};
    int num = 3;
    while(primes.Count < 1000000)
    {
        foreach(int div in primes)
        {
            if ((num / div) * div == num)
                goto next;
        }
        primes.Add(num);
        next:
            ++num;
    }
    return primes.Last();
}

Le compilateur nous en avertira, car il n'y a nulle part où nous pouvons await rien d'utile. Vraiment appeler cela va être la même chose qu'une version légèrement plus compliquée de l'appel de Task.FromResult(CalculateMillionthPrimeNumber()). Cela revient à faire le calcul et puis / la création d’une tâche déjà terminée dont le nombre calculé est le résultat.

Maintenant, les tâches déjà terminées ne sont pas toujours inutiles. Par exemple, considérons:

public async Task<string> GetInterestingStringAsync()
{
    if (_cachedInterestingString == null)
      _cachedInterestingString = await GetStringFromWeb();
    return _cachedInterestingString;
}

Cela retourne une tâche déjà terminée lorsque la chaîne est dans le cache, et non autrement, et dans ce cas, le retour sera assez rapide. D'autres cas peuvent se présenter s'il existe plusieurs implémentations de la même interface et si toutes les implémentations ne peuvent pas utiliser les E/S asynchrones.

Et de même, une méthode async qui awaits cette méthode renverra une tâche déjà terminée ou ne dépendra pas de celle-ci. C'est en fait un très bon moyen de rester sur le même sujet et de faire ce qui doit être fait quand c'est possible.

Mais si c'est toujours possible, alors le seul effet est un peu de folie autour de la création de l'objet Task et de la machine à états que async utilise pour l'implémenter.

Donc, assez inutile. Si c'était ainsi que la version de votre question avait été implémentée, alors calculateMillionthPrimeNumber aurait eu IsCompleted retourné vrai dès le début. Vous devriez juste appeler la version non asynchrone.

Très bien, en tant que développeurs de CalculateMillionthPrimeNumberAsync(), nous voulons faire quelque chose de plus utile pour nos utilisateurs. Donc nous faisons:

public Task<int> CalculateMillionthPrimeNumberAsync()
{
    return Task.Factory.StartNew(CalculateMillionthPrimeNumber, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
}

Ok, maintenant nous ne perdons pas le temps de nos utilisateurs. DoIndependentWork() fera les choses en même temps que CalculateMillionthPrimeNumberAsync(), et si elle se termine en premier, alors la await libérera ce fil.

Génial!

Seulement, nous n'avons pas vraiment déplacé l'aiguille de la position synchrone. En effet, surtout si DoIndependentWork() n’est pas ardu, nous aurions peut-être aggravé la situation. La manière synchrone ferait tout sur un thread, appelons-le Thread A. La nouvelle méthode effectue le calcul sur Thread B, puis publie Thread A, puis se synchronise de plusieurs manières. C'est beaucoup de travail, a-t-il gagné quelque chose?

Eh bien peut-être, mais l'auteur de CalculateMillionthPrimeNumberAsync() ne peut pas le savoir, car les facteurs qui influent sont tous dans le code d'appel. Le code appelant aurait pu faire StartNew lui-même et mieux adapter les options de synchronisation au besoin quand il l'a fait.

Ainsi, alors que les tâches peuvent être un moyen pratique d’appeler du code lié à cpu en parallèle à une autre tâche, les méthodes qui le font ne sont pas utiles. Pire, ils trompent la personne qui voit CalculateMillionthPrimeNumberAsync pourrait être pardonnée de croire que cet appel n'était pas inutile.

4
Jon Hanna

Sauf si CalculateMillionthPrimeNumberAsync utilise constamment async/await seul, il n'y a aucune raison de ne pas laisser la tâche exécuter un travail de processeur lourd, car elle ne fait que déléguer votre méthode au thread de ThreadPool.

Qu'est-ce qu'un thread ThreadPool et en quoi diffère-t-il d'un thread normal? here .

En bref, le thread threadpool est en garde pendant un bon bout de temps (et le nombre de thread threadpool est limité). Ainsi, à moins que vous n'en preniez trop, il n'y a rien à craindre.

1
AgentFire