Je suis nouveau dans la programmation parallèle. Deux classes sont disponibles dans .NET: Task
et Thread
.
Donc, mes questions sont:
Thread
et quand Task
?Thread
est un concept de niveau inférieur: si vous démarrez directement un thread, vous savez ce sera un thread séparé, plutôt que de s'exécuter sur le pool de threads, etc.
Task
est plus qu'une simple abstraction de "où exécuter du code" - c'est en réalité "la promesse d'un résultat dans le futur". Donc, comme quelques exemples différents:
Task.Delay
n'a pas besoin de temps CPU réel; c'est comme mettre une minuterie pour sonner à l'avenirWebClient.DownloadStringTaskAsync
ne prendra pas beaucoup de temps CPU localement; cela représente un résultat susceptible de passer le plus clair de son temps en latence réseau ou en travail à distance (sur le serveur Web)Task.Run()
vraiment est disant "je veux que vous exécutiez ce code séparément"; le thread exact sur lequel ce code s'exécute dépend d'un certain nombre de facteurs.Notez que l'abstraction Task<T>
est essentielle pour la prise en charge asynchrone dans C # 5.
En général, je vous recommande d'utiliser l'abstraction de niveau supérieur chaque fois que vous le pouvez: dans le code C # moderne, il est rarement nécessaire de démarrer explicitement votre propre thread.
Fil de discussion
Thread représente un thread au niveau du système d'exploitation, avec sa propre pile et ses propres ressources du noyau. (techniquement, une implémentation CLR pourrait utiliser des fibres à la place, mais aucun CLR existant ne le fait) Thread autorise le plus haut degré de contrôle; vous pouvez interrompre () ou suspendre () ou reprendre un fil (bien que ce soit une très mauvaise idée), vous pouvez en observer l'état et définir des propriétés au niveau du fil, telles que la taille de la pile, l'état de l'appartement ou la culture.
Le problème avec Thread est que les threads du système d'exploitation sont coûteux. Chaque thread que vous avez utilise une quantité de mémoire non négligeable pour sa pile et ajoute un temps système supplémentaire au processeur en tant que commutateur de contexte de processeur entre les threads. Au lieu de cela, il est préférable de faire exécuter votre code par un petit groupe de threads à mesure que le travail devient disponible.
Il y a des moments où il n'y a pas d'autre fil. Si vous devez spécifier le nom (à des fins de débogage) ou l'état de l'appartement (pour afficher une interface utilisateur), vous devez créer votre propre thread (notez qu'avoir plusieurs threads d'interface utilisateur est généralement une mauvaise idée). De même, si vous souhaitez conserver un objet appartenant à un seul thread et ne pouvant être utilisé que par ce thread, il est beaucoup plus facile de créer explicitement une instance de thread afin que vous puissiez facilement vérifier si le code essayant de l'utiliser est en cours d'exécution. sur le bon fil.
ThreadPool
ThreadPool est un wrapper autour d'un pool de threads gérés par le CLR. ThreadPool ne vous donne aucun contrôle; vous pouvez soumettre le travail à exécuter à un moment donné et vous pouvez contrôler la taille du pool, mais vous ne pouvez rien définir d'autre. Vous ne pouvez même pas dire quand le pool commencera à exécuter le travail que vous lui soumettez.
L'utilisation de ThreadPool évite la surcharge de créer trop de threads. Toutefois, si vous soumettez trop de tâches de longue durée au pool de threads, celui-ci peut être saturé et les travaux que vous soumettez ultérieurement peuvent attendre que les éléments à exécution longue précédents soient terminés. En outre, le ThreadPool n'offre aucun moyen de savoir quand un élément de travail est terminé (contrairement à Thread.Join ()), ni un moyen d'obtenir le résultat. Par conséquent, ThreadPool est mieux utilisé pour les opérations courtes où l'appelant n'a pas besoin du résultat.
Tâche
Enfin, la classe de tâches de la bibliothèque de tâches parallèles offre le meilleur des deux mondes. Comme le ThreadPool, une tâche ne crée pas son propre thread de système d'exploitation. Au lieu de cela, les tâches sont exécutées par un TaskScheduler; le planificateur par défaut s'exécute simplement sur le ThreadPool.
Contrairement au ThreadPool, Task vous permet également de savoir quand il se termine et (via la tâche générique) de renvoyer un résultat. Vous pouvez appeler ContinueWith () sur une tâche existante pour lui faire exécuter davantage de code une fois la tâche terminée (si elle est déjà terminée, le rappel est exécuté immédiatement). Si la tâche est générique, ContinueWith () vous transmettra le résultat de la tâche, vous permettant d'exécuter davantage de code qui l'utilise.
Vous pouvez également attendre de manière synchrone la fin d'une tâche en appelant Wait () (ou, pour une tâche générique, en obtenant la propriété Result). Comme Thread.Join (), cela bloque le thread appelant jusqu'à la fin de la tâche. Attendre une tâche de manière synchrone est généralement une mauvaise idée. cela empêche le thread appelant de faire tout autre travail et peut également entraîner des blocages si la tâche attend en attente (même de manière asynchrone) pour le thread actuel.
Les tâches étant toujours exécutées sur le ThreadPool, elles ne doivent pas être utilisées pour des opérations de longue durée, car elles peuvent toujours remplir le pool de threads et bloquer un nouveau travail. Au lieu de cela, Task fournit une option LongRunning, qui indiquera à TaskScheduler de créer un nouveau thread au lieu de s'exécuter sur le ThreadPool.
Toutes les API de concurrence de niveau supérieur les plus récentes, y compris les méthodes Parallel.For * (), PLINQ, C # 5 wait et les méthodes asynchrones modernes dans BCL, sont toutes construites sur Task.
Conclusion
En fin de compte, cette tâche est presque toujours la meilleure option. il fournit une API beaucoup plus puissante et évite de gaspiller les threads du système d'exploitation.
Les seules raisons pour créer explicitement vos propres threads dans le code moderne sont la définition d'options par thread ou le maintien d'un thread persistant devant conserver sa propre identité.
Habituellement, vous entendez Task est un concept de niveau supérieur au thread ... et c'est ce que cette phrase signifie:
Vous ne pouvez pas utiliser Abort/ThreadAbortedException, vous devez prendre en charge l'événement cancel dans votre "code d'entreprise" testant périodiquement l'indicateur _token.IsCancellationRequested
_ (évitez également les connexions longues ou inépuisables, par exemple à la base de données, sinon vous ne pourrez jamais tester cet indicateur. ). Pour la même raison, l'appel Thread.Sleep(delay)
doit être remplacé par l'appel Task.Delay(delay, token)
.
Il n'y a pas de fonctionnalité de méthodes Suspend
et Resume
du thread avec les tâches. L'instance de tâche ne peut pas être réutilisée non plus.
Mais vous avez deux nouveaux outils:
a) continuations
_// continuation with ContinueWhenAll - execute the delegate, when ALL
// tasks[] had been finished; other option is ContinueWhenAny
Task.Factory.ContinueWhenAll(
tasks,
() => {
int answer = tasks[0].Result + tasks[1].Result;
Console.WriteLine("The answer is {0}", answer);
}
);
_
b) tâches imbriquées/enfants
_//StartNew - starts task immediately, parent ends whith child
var parent = Task.Factory.StartNew
(() => {
var child = Task.Factory.StartNew(() =>
{
//...
});
},
TaskCreationOptions.AttachedToParent
);
_
Ainsi, le thread système est complètement caché de la tâche, mais le code de la tâche est toujours exécuté dans le thread système concret. Les threads système sont des ressources pour les tâches et, bien entendu, il reste un pool de threads sous le capot de l'exécution parallèle de la tâche. Il peut y avoir différentes stratégies pour que le thread obtienne de nouvelles tâches à exécuter. Une autre ressource partagée TaskScheduler s'en préoccupe. Certains problèmes que TaskScheduler résout 1) préfèrent exécuter la tâche et sa confirmation dans le même fil en réduisant les coûts de commutation - aka exécution en ligne ) 2) préfèrent exécuter les tâches dans l’ordre dans lequel elles ont été démarrées - alias PreferFairness 3) une distribution plus efficace des tâches entre les threads inactifs en fonction de la "connaissance préalable de l’activité des tâches "- aka Vol de travail . Important: en général, "async" n'est pas identique à "parallèle". En jouant avec les options de TaskScheduler, vous pouvez configurer les tâches asynchrones à exécuter dans un thread de manière synchrone. Pour exprimer l'exécution de code parallèle, des abstractions plus élevées (que Tasks) peuvent être utilisées: Parallel.ForEach
, PLINQ
, Dataflow
.
Les tâches sont intégrées aux fonctions asynchrones/wait C # Promise Model , par exemple, il requestButton.Clicked += async (o, e) => ProcessResponce(await client.RequestAsync(e.ResourceName));
l'exécution de _client.RequestAsync
_ ne bloquera pas l'interface utilisateur fil. Important: sous le capot, l'appel de délégué Clicked
est absolument régulier (tous les threads sont effectués par le compilateur).
C'est assez pour faire un choix. Si vous devez prendre en charge la fonctionnalité d'annulation d'appels de l'API héritée qui a tendance à se bloquer (par exemple, une connexion sans délai) et supporte dans ce cas Thread.Abort (), ou si vous créez des calculs d'arrière-plan multithreads et souhaitez optimiser la commutation entre les threads à l'aide de Suspend/Resume , c’est-à-dire gérer manuellement l’exécution parallèle - restez avec Thread. Sinon, allez dans Tasks car ils vous manipuleront facilement, seront intégrés dans le langage et rendront les développeurs plus productifs - Task Parallel Library (TPL) .
La classe Thread
est utilisée pour créer et manipuler un thread sous Windows.
A Task
représente une opération asynchrone et fait partie de la bibliothèque parallèle de tâches , un ensemble d’API permettant d’exécuter des tâches de manière asynchrone et en parallèle.
Autrefois (c'est-à-dire avant TPL), l'utilisation de la classe Thread
était l'une des méthodes standard pour exécuter du code en arrière-plan ou en parallèle (une meilleure alternative consistait souvent à utiliser un ThreadPool
), toutefois, cette procédure était lourde et présentait plusieurs inconvénients, dont le moindre était le temps système de création d'un nouveau thread pour effectuer une tâche en arrière-plan.
De nos jours, l’utilisation de tâches et de TPL est une solution bien meilleure dans 90% des cas, car elle fournit des abstractions qui permettent une utilisation beaucoup plus efficace des ressources système. J'imagine qu'il existe quelques scénarios dans lesquels vous souhaitez un contrôle explicite sur le thread sur lequel vous exécutez votre code. Toutefois, si vous souhaitez exécuter quelque chose de manière asynchrone, votre premier port d'escale devrait être la TPL.