67 votes

Question sur la fin d'un thread proprement dans .NET

Je comprends que Thread.Abort() est diabolique d'après la multitude d'articles que j'ai lus sur le sujet, donc je suis actuellement en train d'arracher tous mes abort() afin de les remplacer par une méthode plus propre ; et après avoir comparé les stratégies des utilisateurs ici sur stackoverflow, puis après avoir lu " Comment faire : Créer et terminer des threads (Guide de programmation C#) " de MSDN, qui proposent tous deux une approche très similaire, à savoir l'utilisation d'une volatile bool stratégie de vérification de l'approche, ce qui est bien, mais j'ai encore quelques questions.....

Ce qui me frappe immédiatement ici, c'est ce qui se passe si vous n'avez pas un simple processus de travailleur qui exécute simplement une boucle de code de crunching ? Par exemple, pour moi, mon processus est un processus de téléchargement de fichiers en arrière-plan, je fais en fait une boucle à travers chaque fichier, donc c'est quelque chose, et bien sûr je pourrais ajouter mon while (!_shouldStop) au sommet qui me couvre chaque itération de boucle, mais j'ai beaucoup d'autres processus d'affaires qui se produisent avant qu'il atteigne sa prochaine itération de boucle, je veux que cette procédure d'annulation soit rapide ; ne me dites pas que je dois saupoudrer ces boucles while toutes les 4-5 lignes vers le bas dans toute ma fonction de travailleur ?!

J'espère vraiment qu'il existe une meilleure solution. Quelqu'un pourrait-il me dire s'il s'agit de l'approche correcte [et unique] ou s'il a utilisé des stratégies dans le passé pour obtenir ce que je cherche ?

Merci à tous.

Pour en savoir plus : Toutes ces réponses SO suppose que le fil de travail va tourner en boucle. Je ne suis pas à l'aise avec cela. Et s'il s'agit d'une opération d'arrière-plan linéaire, mais opportune ?

2 votes

Pensez-y de cette façon : voulez-vous vraiment que votre processus de longue durée s'arrête à n'importe quel moment arbitraire alors qu'il aurait pu être au milieu d'un traitement modifiant l'état ? La raison pour laquelle vous devez ajouter des vérifications au milieu du code (pas nécessairement dans une boucle, bien que cela soit courant) est que vous seul, en tant que développeur, connaissez suffisamment votre code pour savoir quand il est sûr de s'arrêter. Si le thread se bloque en attendant des signaux de synchronisation, consultez la méthode Interrupt() mentionnée dans les réponses.

0 votes

Je ne nie pas qu'il ne devrait pas être géré par l'utilisateur (moi), mais il me semblait simplement que c'était beaucoup de code superflu. Ce serait bien de mettre en place quelque chose comme la façon dont un try, catch s'exécute, en surveillant constamment, dès qu'un drapeau est faux, puis peut-être return; de la fonction. Je ne sais pas.

0 votes

Il s'avère que ce que je voulais, c'était Thread.Interrupt . Bien que je pense que je préfère une vérification du drapeau bool, du moins pour l'instant, afin de garantir absolument l'intégrité de l'état de mon application.

108voto

Brian Gideon Points 26683

Malheureusement, il n'y a peut-être pas de meilleure option. Cela dépend vraiment de votre scénario spécifique. L'idée est d'arrêter le fil de discussion de manière élégante à des points sûrs. C'est la raison principale pour laquelle Thread.Abort n'est pas bon, car il n'est pas garanti qu'il se produise à des moments sûrs. En saupoudrant le code d'un mécanisme d'arrêt, vous définissez effectivement manuellement les points sûrs. C'est ce qu'on appelle l'annulation coopérative. Il existe quatre grands mécanismes pour ce faire. Vous pouvez choisir celui qui convient le mieux à votre situation.

Interroger un drapeau d'arrêt

Vous avez déjà mentionné cette méthode. C'est une méthode assez courante. Effectuez des vérifications périodiques du drapeau à des points sûrs de votre algorithme et renoncez à l'utiliser lorsqu'il est signalé. L'approche standard consiste à marquer la variable volatile . Si cela n'est pas possible ou peu commode, vous pouvez utiliser une lock . Rappelez-vous que vous ne pouvez pas marquer une variable locale comme volatile Ainsi, si une expression lambda la capture par le biais d'une fermeture, par exemple, vous devrez recourir à une méthode différente pour créer la barrière mémoire requise. Il n'y a pas grand-chose d'autre à dire sur cette méthode.

Utiliser les nouveaux mécanismes d'annulation dans le TPL

C'est similaire à l'interrogation d'un drapeau d'arrêt, sauf qu'il utilise les nouvelles structures de données d'annulation dans la TPL. Elle est toujours basée sur des modèles d'annulation coopératifs. Vous devez obtenir un CancellationToken et le contrôle périodique IsCancellationRequested . Pour demander l'annulation, vous devez appeler Cancel sur le CancellationTokenSource qui a fourni le jeton à l'origine. Il y a beaucoup de choses que vous pouvez faire avec les nouveaux mécanismes d'annulation. Vous pouvez en savoir plus sur ici .

Utiliser les poignées d'attente

Cette méthode peut être utile si votre fil de travail doit attendre un intervalle spécifique ou un signal pendant son fonctionnement normal. Vous pouvez Set a ManualResetEvent par exemple, pour faire savoir au fil qu'il est temps de s'arrêter. Vous pouvez tester l'événement en utilisant la fonction WaitOne qui renvoie un bool indiquant si l'événement a été signalé. Le site WaitOne prend un paramètre qui spécifie le temps d'attente pour le retour de l'appel si l'événement n'a pas été signalé dans ce laps de temps. Vous pouvez utiliser cette technique à la place de Thread.Sleep et obtenir l'indication d'arrêt en même temps. Il est également utile s'il y a d'autres WaitHandle que le thread peut avoir à attendre. Vous pouvez appeler WaitHandle.WaitAny pour attendre n'importe quel événement (y compris l'événement d'arrêt) en un seul appel. Utiliser un événement peut être plus efficace que d'appeler Thread.Interrupt puisque vous avez plus de contrôle sur le déroulement du programme ( Thread.Interrupt lève une exception, il faut donc placer stratégiquement l'option try-catch pour effectuer tout nettoyage nécessaire).

Scénarios spécialisés

Il y a plusieurs scénarios ponctuels qui ont des mécanismes d'arrêt très spécialisés. Il n'entre pas dans le cadre de cette réponse de les énumérer tous (sans compter que ce serait presque impossible). Un bon exemple de ce que je veux dire ici est le scénario Socket la classe. Si le thread est bloqué lors d'un appel à Send o Receive puis en appelant Close interrompra la socket sur n'importe quel appel bloquant dans lequel elle se trouvait, la débloquant effectivement. Je suis sûr qu'il y a plusieurs autres endroits dans la BCL où des techniques similaires peuvent être utilisées pour débloquer un thread.

Interrompre le fil de discussion via Thread.Interrupt

L'avantage ici est qu'il est simple et que vous ne devez pas vous attacher à saupoudrer votre code de quoi que ce soit. L'inconvénient est que vous avez peu de contrôle sur l'emplacement des points sûrs dans votre algorithme. La raison en est que Thread.Interrupt fonctionne en injectant une exception dans l'un des appels bloquants de la BCL. Ces appels comprennent Thread.Sleep , WaitHandle.WaitOne , Thread.Join etc. Vous devez donc faire attention à l'endroit où vous les placez. Cependant, la plupart du temps, c'est l'algorithme qui dicte leur emplacement et c'est généralement bien, surtout si votre algorithme passe la plupart de son temps dans l'un de ces appels bloquants. Si votre algorithme n'utilise pas l'un des appels bloquants de la BCL, cette méthode ne vous conviendra pas. La théorie ici est que le ThreadInterruptException est seulement généré à partir de l'appel d'attente .NET, il est donc probablement à un point sûr. Vous savez au moins que le thread ne peut pas se trouver dans du code non géré ou sortir d'une section critique en laissant un verrou suspendu dans un état acquis. Bien que cela soit moins invasif que Thread.Abort Je déconseille toujours son utilisation car il n'est pas évident de savoir quels appels y répondent et de nombreux développeurs ne seront pas familiers avec ses nuances.

0 votes

Très instructif et donne de très bons exemples. Merci beaucoup.

0 votes

Points attribués pour la diversité des réponses. Merci.

0 votes

Si un thread appelle une méthode non gérée qui peut prendre un temps excessivement long et qui devrait être tuable (en reconnaissant que tuer la DLL peut la laisser dans un mauvais état, mais ne devrait pas affecter quoi que ce soit d'autre que lui-même), quel serait l'effet d'avoir le thread qui acquiert un verrou et le maintient à tout moment. sauf quand il serait sûr de tuer ? Est-ce qu'un thread extérieur qui a acquis ce verrou peut utiliser sans danger Thread.Abort puisque le fil à interrompre ne pouvait pas être en mauvaise posture (ou alors il tiendrait la serrure) ?

13voto

Lirik Points 17868

Malheureusement, en multithreading, il faut souvent faire un compromis entre la rapidité et la propreté... vous pouvez quitter un thread immédiatement si vous... Interrupt mais ce ne sera pas très propre. Donc non, vous n'avez pas besoin de saupoudrer le _shouldStop toutes les 4 à 5 lignes, mais si vous interrompez votre thread, vous devez gérer l'exception et sortir de la boucle de manière propre.

Mise à jour

Même s'il ne s'agit pas d'un thread en boucle (c'est-à-dire s'il s'agit d'un thread qui effectue une opération asynchrone de longue durée ou un type de bloc pour une opération d'entrée), vous pouvez Interrupt mais vous devriez quand même attraper le ThreadInterruptedException et quitter le fil proprement. Je pense que les exemples que vous avez lus sont très appropriés.

Mise à jour 2.0

Oui, j'ai un exemple... Je vais juste vous montrer un exemple basé sur le lien que vous avez référencé :

public class InterruptExample
{
    private Thread t;
    private volatile boolean alive;

    public InterruptExample()
    {
        alive = false;

        t = new Thread(()=>
        {
            try
            {
                while (alive)
                {
                    /* Do work. */
                }
            }
            catch (ThreadInterruptedException exception)
            {
                /* Clean up. */
            }
        });
        t.IsBackground = true;
    }

    public void Start()
    {
        alive = true;
        t.Start();
    }

    public void Kill(int timeout = 0)
    {
        // somebody tells you to stop the thread
        t.Interrupt();

        // Optionally you can block the caller
        // by making them wait until the thread exits.
        // If they leave the default timeout, 
        // then they will not wait at all
        t.Join(timeout);
    }
}

0 votes

Avez-vous un exemple de l'utilisation d'une interruption ? Merci Lirik.

0 votes

@GONeale, j'ai posté un exemple pour vous maintenant... faites-moi savoir si c'est utile.

0 votes

Ah maintenant, ça a du sens. J'avais l'intuition que cela levait une exception à n'importe quel moment, et l'idée était que nous devions l'attraper. Ok, je vais prendre une décision entre les solutions proposées, merci.

9voto

blucz Points 1129

Si l'annulation est une exigence de la chose que vous construisez, alors elle doit être traitée avec autant de respect que le reste de votre code - c'est peut-être quelque chose que vous devez concevoir.

Partons du principe que votre fil fait l'une des deux choses suivantes à tout moment.

  1. Quelque chose lié au CPU
  2. Attendre le noyau

Si vous êtes lié au CPU dans le fil en question, vous probablement avoir un bon endroit pour insérer le chèque de caution. Si vous faites appel au code de quelqu'un d'autre pour effectuer une tâche longuement liée au CPU, vous devrez peut-être corriger le code externe, le déplacer hors du processus (l'abandon des threads est diabolique, mais l'abandon des processus est bien défini et sûr), etc.

Si vous attendez le noyau, alors il y a probablement un handle (ou fd, ou port mach, ...) impliqué dans l'attente. Habituellement, si vous détruisez le handle en question, le noyau retournera immédiatement avec un code d'échec. Si vous êtes en .net/java/etc. vous finirez probablement avec une exception. En C, le code que vous avez déjà mis en place pour gérer les échecs des appels système propagera l'erreur jusqu'à une partie significative de votre application. D'une manière ou d'une autre, vous sortez de l'endroit de bas niveau assez proprement et de manière très opportune sans avoir besoin d'un nouveau code parsemé partout.

Une tactique que j'utilise souvent avec ce type de code est de garder la trace d'une liste de poignées qui doivent être fermées, puis de faire en sorte que ma fonction d'interruption active un drapeau "annulé" et les ferme. Lorsque la fonction échoue, elle peut vérifier l'indicateur et signaler l'échec dû à l'annulation plutôt qu'à l'exception/errno spécifique.

Vous semblez sous-entendre qu'une granularité acceptable pour l'annulation se situe au niveau d'un appel de service. Ce n'est probablement pas une bonne idée - il vaut mieux annuler le travail en arrière-plan de manière synchrone et joindre l'ancien fil d'arrière-plan au fil d'avant-plan. C'est beaucoup plus propre car :

  1. Il évite une classe de conditions de course lorsque les anciens threads de bgwork reviennent à la vie après des délais inattendus.

  2. Il évite les fuites potentielles de threads/de mémoire cachés causées par les processus d'arrière-plan suspendus en permettant aux effets d'un thread d'arrière-plan suspendu de se cacher.

Il y a deux raisons d'avoir peur de cette approche :

  1. Vous ne pensez pas pouvoir interrompre votre propre code en temps voulu. Si l'annulation est une exigence de votre application, la décision que vous devez vraiment prendre est une décision de ressource/entreprise : faire un hack, ou résoudre votre problème proprement.

  2. Vous ne faites pas confiance à un code que vous appelez parce qu'il est hors de votre contrôle. Si vous n'avez vraiment pas confiance, envisagez de le déplacer hors du processus. Vous obtiendrez une bien meilleure isolation contre de nombreux types de risques, y compris celui-ci, de cette façon.

2 votes

Il est bon de souligner que l'annulation doit être une décision de conception (lorsque cela est possible - ce n'est pas toujours le cas si vous n'avez pas écrit le code sous-jacent), et non une réflexion après coup.

2voto

Brent Arias Points 7104

Peut-être qu'une partie du problème vient du fait que votre méthode / boucle while est si longue. Que vous ayez ou non des problèmes de threading, vous devriez le décomposer en petites étapes de traitement. Supposons que ces étapes soient Alpha(), Bravo(), Charlie() et Delta().

Vous pourriez alors faire quelque chose comme ceci :

    public void MyBigBackgroundTask()
    {
        Action[] tasks = new Action[] { Alpha, Bravo, Charlie, Delta };
        int workStepSize = 0;
        while (!_shouldStop)
        {
            tasks[workStepSize++]();
            workStepSize %= tasks.Length;
        };
    }

Alors oui, il tourne en boucle sans fin, mais vérifie s'il est temps de s'arrêter entre chaque étape de l'activité.

0 votes

Ok, merci. Mais cela revient à la même approche. Pas de problème.

2voto

Chris Lively Points 59564

Vous n'avez pas besoin de saupoudrer des boucles partout. La boucle while externe vérifie simplement si on lui a dit de s'arrêter et si c'est le cas, elle ne fait pas d'autre itération...

Si vous avez un fil de discussion direct (sans boucle), il suffit de vérifier le booléen _shouldStop avant ou après chaque point important du fil de discussion. De cette façon, vous savez si vous devez continuer ou vous arrêter.

par exemple :

public void DoWork() {

  RunSomeBigMethod();
  if (_shouldStop){ return; }
  RunSomeOtherBigMethod();
  if (_shouldStop){ return; }
  //....
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X