async attendent les performances?
(Juste une question théorique - pour les applications à interface graphique)
En supposant que j'ai ce code avec beaucoup awaits
:
public async Task<T> ConsumeAsync()
{
await A();
await b();
await c();
await d();
//..
}
Où chaque tâche peut prendre très peu de temps,
Question (encore une fois , théorique)
Il peut être une situation où les globale du temps à s'occuper, avec tous ces "libération threads" et "chercher threads" ( rouge et vert ici :)
Prend plus de de temps qu'un seul thread qui pourrait faire tout le travail avec un peu de retard ,
Je veux dire, je voulais être le plus productif , mais à la place, puisque tous ces commutateurs d'avant en arrière - j'ai en fait perdu la productivité.
Un tel scénario peut-il se produire ?
5 réponses
Un objet Task
représente le résultat différé d'une opération en attente. Vous n'avez pas besoin d'utiliser tasks et async/await
Si vous n'avez pas d'opérations en attente. Sinon, je crois async
/await
le code est généralement plus efficace que son analogue NU TPL ContinueWith
.
Faisons un peu de timing:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication
{
class Program
{
// async/await version
static async Task<int> Test1Async(Task<int> task)
{
return await task;
}
// TPL version
static Task<int> Test2Async(Task<int> task)
{
return task.ContinueWith(
t => t.Result,
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
static void Tester(string name, Func<Task<int>, Task<int>> func)
{
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
for (int i = 0; i < 10000000; i++)
{
func(Task.FromResult(0)).Wait();
}
sw.Stop();
Console.WriteLine("{0}: {1}ms", name, sw.ElapsedMilliseconds);
}
static void Main(string[] args)
{
Tester("Test1Async", Test1Async);
Tester("Test2Async", Test2Async);
}
}
}
La sortie:
Test1Async: 1582ms Test2Async: 4975ms
Ainsi, par défaut, await
continuations sont traitées plus efficacement que ContinueWith
continuations. Optimisons légèrement ce code:
// async/await version
static async Task<int> Test1Async(Task<int> task)
{
if (task.IsCompleted)
return task.Result;
return await task;
}
// TPL version
static Task<int> Test2Async(Task<int> task)
{
if (task.IsCompleted)
return Task.FromResult(task.Result);
return task.ContinueWith(
t => t.Result,
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
Le sortie:
Test1Async: 1557ms Test2Async: 429ms
Maintenant, la version non asynchrone gagne. Dans le cas de la version async
, je crois que cette optimisation a déjà été effectuée en interne par l'infrastructure async/await
.
Quoi qu'il en soit, jusqu'à présent, nous n'avons traité que des tâches terminées (Task.FromResult
). Introduisons l'asynchronie réelle (naturellement, nous ferons moins d'itérations cette fois):
static Task<int> DoAsync()
{
var tcs = new TaskCompletionSource<int>();
ThreadPool.QueueUserWorkItem(_ => tcs.SetResult(0));
return tcs.Task;
}
static void Tester(string name, Func<Task<int>, Task<int>> func)
{
ThreadPool.SetMinThreads(200, 200);
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
for (int i = 0; i < 1000000; i++)
{
func(DoAsync()).Wait();
}
sw.Stop();
Console.WriteLine("{0}: {1}ms", name, sw.ElapsedMilliseconds);
}
La sortie:
Test1Async: 4207ms Test2Async: 4734ms
Maintenant, la différence est très marginale, bien que la version async
fonctionne encore légèrement mieux. Pourtant, Je pensez qu'un tel gain est vraiment négligeable, comparable au coût réel de l'opération asynchrone ou au coût de la restauration du contexte capturé pour when SynchronizationContext.Current != null
.
L'essentiel est, si vous traitez des tâches asynchrones, optez pour async
/await
Si vous avez le choix, pas pour des raisons de performance mais pour la facilité d'utilisation, la lisibilité et la maintenabilité.
Oui, en théorie. Normalement pas, dans le monde réel.
Dans le cas courant, async
est utilisé pour les opérations liées aux E/S, et la surcharge de la gestion des threads est indétectable par rapport à eux. La plupart du temps, les opérations asynchrones prennent beaucoup de temps (par rapport à la gestion des threads) ou sont déjà terminées (par exemple, un cache). Notez que async
a un "chemin rapide" qui débute si l'opération est déjà terminée, où il ne donne pas le thread.
Pour plus informations, voir le Zen de Async et Async Performance .
Oui, ça peut arriver. N'oubliez pas non plus que - toute l'efficacité que vous pouvez programmer - le système de tâches a des frais généraux.
Si vous obtenez trop granulkar avec quelque chose comme ça, la surcharge de synchronisation peut vous tuer. Cela dit: les tâches sont programmées de manière assez efficace.
Mais l'ancienne règle colle: ne pas aller super granulaire. Parfois, l'optimisation aide.
Un tel scénario peut-il se produire?
Absolument. Pour cette raison, vous devriez être consciencieux sur l'endroit où vous utilisez du code asynchrone. En règle générale, il est préférable de l'utiliser pour les méthodes qui effectueront une opération asynchrone (E/S disque ou réseau, par exemple). Le temps que prennent ces opérations l'emporte généralement sur le coût de la planification des tâches sur les threads. En outre, au niveau du système d'exploitation, ces types d'opérations sont intrinsèquement asynchrones, donc vous êtes réellement Suppression de une couche d'abstraction en utilisant des méthodes asynchrones.
Même dans ces cas, vous ne verrez probablement pas de différence de performance notable en passant au code asynchrone à moins que vous ne puissiez profiter de la concurrence. Par exemple, le code que vous avez posté ne verrait probablement aucun gain de performance réel à moins qu'il ne soit changé en quelque chose comme ceci:
await Task.WhenAll(new[]{A(), B(), C(), D(), ...});
Oui, bien sûr que cela peut arriver. Avec tous les frais généraux de la création de la machine d'état, en cédant le contrôle et en utilisant IOCP
threads. Mais comme dit, le TPL
est assez optimisé. Par exemple, n'oublions pas que si votre TaskAwaitable
se termine rapidement, il se peut qu'il n'y ait pas de surcharge et qu'il s'exécute de manière synchrone, ce qui peut arriver souvent avec des opérations rapides.