Une Task est un objet qui représente un travail en cours. Elle peut s'exécuter sur un thread du pool, attendre un timer ou attendre une réponse réseau ; dans tous les cas, vous pouvez demander si elle est terminée, obtenir son résultat, l'attendre ou lui rattacher d'autre travail. async et await sont la syntaxe du langage au-dessus des tâches. Cette page couvre l'API Task elle-même, ainsi que les threads.
Task.Run : du travail sur le pool de threads
Task.Run prend une lambda et la met en file sur le pool de threads, un ensemble de threads que .NET garde sous la main pour ne pas en créer un nouveau à chaque tâche. Il renvoie une Task, ou une Task<T> si la lambda renvoie une valeur.
Sortie :
Started, completed yet: False
Primes up to 200,000: 17984
Status: RanToCompletion
Result bloque le thread appelant jusqu'à la fin de la tâche. C'est acceptable dans le Main d'un programme console, mais dans du code async vous écririez int primes = await task;, qui attend sans bloquer. Il en va de même pour task.Wait() sur une Task sans résultat.
Task.Run sert au travail lié au CPU : des calculs qui occupent un cœur. Pour les E/S, comme lire un fichier ou appeler une API web, utilisez la méthode async que fournit déjà la bibliothèque (File.ReadAllTextAsync, HttpClient.GetStringAsync) et attendez-la directement avec await. Pendant que l'E/S est en cours, un appel async attendu ne retient aucun thread : l'envelopper dans Task.Run n'apporte rien et ajoute seulement un passage par le pool de threads. Ce qui occupe vraiment un thread du pool, c'est Task.Run autour d'un appel synchrone bloquant comme File.ReadAllText : ce thread reste inactif jusqu'à ce que le disque ou le réseau réponde.
Plusieurs tâches à la fois : WhenAll
Découpez un travail indépendant en tâches, démarrez-les toutes et combinez les résultats avec Task.WhenAll. Sur une machine à plusieurs cœurs, les morceaux s'exécutent en parallèle.
Sortie :
Parts: 14999995, 14999999, 15000003, 15000000
Total: 59999997
Chaque morceau travaille sur sa propre plage et renvoie sa propre somme, donc les tâches ne partagent rien et n'ont besoin d'aucun verrou. WhenAll renvoie les résultats dans l'ordre des tâches, quel que soit l'ordre dans lequel les threads ont terminé. Quand des tâches doivent mettre à jour un état partagé, voir lock.
WhenAny et délais d'expiration
Task.WhenAny se termine quand la première de ses tâches se termine et renvoie cette tâche. Mettre le vrai travail en concurrence avec Task.Delay donne un délai d'expiration :
Sortie :
fast mirror answered after 100 ms
Timed out after 300 ms
WhenAny renvoie la tâche gagnante elle-même, pas son résultat, donc vous l'attendez à nouveau avec await pour obtenir la valeur (ou l'exception, si elle a échoué). Notez que les tâches perdantes continuent de s'exécuter ; WhenAny ne les arrête pas. Pour arrêter réellement le travail lent, annulez-le avec un jeton comme montré plus bas. Depuis .NET 6, await work.WaitAsync(TimeSpan.FromMilliseconds(300)) exprime le même délai en un seul appel.
Continuations avec ContinueWith
Avant await, la façon d'exécuter du code après la fin d'une tâche était ContinueWith. Vous le verrez encore dans du code plus ancien :
Sortie :
Report total: 42
Chaque continuation reçoit la tâche précédente et s'exécute quand celle-ci se termine. await fait la même chose avec beaucoup moins de cérémonie, gère naturellement les exceptions et revient au bon contexte ; préférez-le donc dans le code nouveau. ContinueWith s'exécute même quand la tâche précédente a échoué ou a été annulée, donc le code plus ancien doit examiner t.IsFaulted ou passer des TaskContinuationOptions pour éviter d'appeler .Result sur une tâche en échec.
Annulation avec CancellationToken
Les tâches s'annulent de façon coopérative. Un CancellationTokenSource émet un jeton ; le code qui fait le travail vérifie le jeton et s'arrête lui-même. Rien n'est interrompu de force.
Sortie :
Cancelled before finishing all 10 orders
Processed 3
Le jeton est transmis à travers chaque couche, et les API async comme Task.Delay, HttpClient et les appels de base de données l'acceptent aussi, si bien qu'une annulation les arrête en pleine attente. Interceptez OperationCanceledException ; la TaskCanceledException que lèvent certaines API en dérive. CancellationTokenSource est IDisposable, d'où le using.
Thread ou Task
Un Thread est un thread du système d'exploitation que vous créez vous-même. Il est plus lourd : chacun a sa propre pile (1 Mo par défaut sous Windows), en démarrer un prend un temps mesurable, et il ne vous donne ni résultat ni gestion des exceptions.
Sortie :
Thread started
Result: 42
Thread | Task | |
|---|---|---|
| S'exécute sur | Un nouveau thread système dédié | Un thread du pool (ou aucun thread, pour les E/S) |
| Coût de démarrage | Élevé | Faible |
| Valeur de résultat | Non, partagez une variable | Task<T>.Result / await |
| Exceptions | Celles non gérées font planter le processus | Stockées dans la tâche, relevées à l'await |
| Annulation | Indicateurs manuels | CancellationToken intégré |
| L'attendre | Join() | await, Wait(), WhenAll |
N'utilisez Thread que lorsqu'il vous faut quelque chose que le pool ne donne pas : une boucle à longue durée de vie qui ne doit jamais emprunter un thread du pool, une priorité particulière, ou un appartement monothread pour COM. Pour les tâches de longue durée, Task.Factory.StartNew(work, TaskCreationOptions.LongRunning) obtient un thread dédié tout en gardant l'API des tâches.
Parallel.For et Parallel.ForEach
Pour « faire ceci sur chaque élément en utilisant tous les cœurs », System.Threading.Tasks.Parallel est plus simple que de créer des tâches à la main. Il répartit la plage entre les threads et revient quand toutes les itérations sont terminées :
Exemple de sortie :
image 4 on thread 7
image 3 on thread 6
image 5 on thread 8
image 0 on thread 1
image 1 on thread 4
image 2 on thread 5
Total pixels: 385425
Les lignes par image sortent dans l'ordre où les threads les ont exécutées, et les identifiants de threads varient d'une exécution à l'autre ; seul le total est fixe. C'est la nature des boucles parallèles : les itérations ne doivent pas dépendre de leur ordre mutuel, et tout état partagé demande une synchronisation (ici Interlocked.Add). Parallel.ForEach fait de même sur n'importe quelle collection, et .NET 6 ajoute Parallel.ForEachAsync pour les corps async.
Erreurs courantes
.Resultou.Wait()dans du code async. Cela bloque un thread et peut provoquer un interblocage dans les applications d'interface ; utilisezawait.- Intercepter le type d'exception d'origine autour de
.Result. Les échecs arrivent enveloppés dans uneAggregateException;awaitles déballe. - Lancer et oublier.
Task.Run(...)sans attendre ni stocker la tâche perd ses exceptions. Task.Runautour d'E/S. Autour d'une méthode async, il ajoute seulement un passage par le pool de threads ; autour d'une méthode bloquante commeFile.ReadAllText, il immobilise un thread du pool pendant toute l'attente. Attendez directement la méthode d'E/S async.- Ignorer le jeton. Passer un
CancellationTokenne sert à rien si le travail ne le vérifie pas. - Un état partagé non synchronisé dans du code parallèle. Utilisez
Interlocked,lock, ou donnez à chaque tâche ses propres données.
Questions fréquentes
Qu'est-ce qu'une Task en C# ?
Une Task représente une opération qui peut se terminer plus tard : du code exécuté sur le pool de threads, un timer, une requête d'E/S. On peut l'attendre avec await, elle indique si elle s'est terminée, a échoué ou a été annulée, et Task<T> porte aussi un résultat. Les tâches sont la brique de base sous async/await.
Quelle est la différence entre une Task et un Thread en C# ?
Un Thread est un thread du système d'exploitation que vous créez et gérez vous-même, avec sa propre pile ; il coûte cher à démarrer et ne renvoie aucune valeur. Une Task décrit un travail, et Task.Run le planifie sur un pool partagé de threads réutilisables ; elle peut renvoyer un résultat, propager des exceptions, être annulée et être attendue. Utilisez des tâches, sauf si vous avez besoin d'un thread dédié à longue durée de vie avec des réglages particuliers.
Quand utiliser Task.Run ?
Pour le travail lié au CPU que vous voulez sortir du thread courant : traitement d'images, gros calcul, analyse d'un grand fichier en mémoire, surtout pour garder une interface réactive. N'enveloppez pas d'E/S dans Task.Run : les méthodes d'E/S async comme ReadAllTextAsync libèrent déjà le thread pendant l'attente.
Comment annuler une Task en C# ?
Créez un CancellationTokenSource, passez son Token au travail, et appelez Cancel() (ou CancelAfter(timeout)) sur la source. Le travail doit coopérer : vérifiez token.IsCancellationRequested ou appelez token.ThrowIfCancellationRequested() dans sa boucle, et transmettez le jeton aux API async comme Task.Delay. Attendre une tâche annulée lève OperationCanceledException.
Quelle est la différence entre Task.WhenAll et Task.WhenAny ?
Task.WhenAll se termine quand toutes les tâches sont terminées et vous donne tous les résultats. Task.WhenAny se termine dès que la première tâche se termine et renvoie cette tâche. WhenAny est la façon habituelle d'ajouter un délai d'expiration : mettre le travail en concurrence avec Task.Delay.
Pourquoi Task.Result lève-t-il AggregateException ?
.Result et .Wait() enveloppent tout échec dans une AggregateException, car une tâche peut en général contenir plusieurs exceptions. await la déballe et relève directement la première exception interne, ce qui est une raison de préférer await. Si vous devez bloquer, .GetAwaiter().GetResult() lève aussi l'exception d'origine.