web-dev-qa-db-fra.com

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

Je comprends que Thread.Abort () est mauvais à cause de la multitude d'articles que j'ai lus sur le sujet, donc je suis actuellement en train d'extraire de mes avortements afin de le remplacer de manière plus propre; et après avoir comparé les stratégies utilisateur des personnes présentes sur stackoverflow, puis après avoir lu " Comment: créer et terminer des threads (Guide de programmation C #) " à partir de MSDN, qui indiquent tous deux une approche très similaire - qui consiste à utiliser une stratégie de vérification d'approche volatile bool, ce qui est bien, mais j'ai encore quelques questions ....

Immédiatement, ce qui me frappe ici, c'est que si vous n'avez pas un processus de travail simple qui exécute simplement une boucle de code 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 dans chaque fichier, donc c'est quelque chose, et bien sûr je pourrais ajouter ma while (!_shouldStop) en haut qui me couvre chaque itération de boucle, mais j'ai beaucoup plus de processus commerciaux qui se produisent avant qu'il ne frappe sa prochaine itération de boucle, je veux que cette procédure d'annulation soit rapide; ne me dites pas que je dois les saupoudrer tout en bouclant toutes les 4-5 lignes vers le bas tout au long de ma fonction de travailleur?!

J'espère vraiment qu'il y a une meilleure façon, est-ce que quelqu'un pourrait me conseiller s'il s'agit en fait de la bonne approche [et seulement?] Pour le faire, ou des stratégies qu'ils ont utilisées dans le passé pour réaliser ce que je recherche.

Merci gang.

Lectures complémentaires: Toutes ces réponses SO supposent que le thread de travail va boucler. Cela ne me convient pas. Et si c'est une opération d'arrière-plan linéaire mais opportune ?

58
GONeale

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 thread gracieusement à des points sûrs. C'est le nœud de la raison pour laquelle Thread.Abort n'est pas bon; car il n'est pas garanti de se produire à des points sûrs. En saupoudrant le code avec un mécanisme d'arrêt, vous définissez effectivement manuellement les points de sécurité. C'est ce qu'on appelle l'annulation coopérative. Il existe essentiellement 4 grands mécanismes pour ce faire. Vous pouvez choisir celui qui correspond le mieux à votre situation.

Interroger un indicateur d'arrêt

Vous avez déjà mentionné cette méthode. C'est assez commun. Effectuez des vérifications périodiques du drapeau à des points sûrs de votre algorithme et renflouez-le lorsqu'il est signalé. L'approche standard consiste à marquer la variable volatile. Si cela n'est pas possible ou incommode, vous pouvez utiliser un lock. N'oubliez pas que vous ne pouvez pas marquer une variable locale comme volatile, donc 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 de mémoire requise. Il n'y a pas grand-chose d'autre à dire pour cette méthode.

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

Ceci est similaire à l'interrogation d'un indicateur d'arrêt, sauf qu'il utilise les nouvelles structures de données d'annulation dans le TPL. Elle repose toujours sur des modèles d'annulation coopératifs. Vous devez obtenir un CancellationToken et vérifier périodiquement IsCancellationRequested. Pour demander l'annulation, vous devez appeler Cancel sur le CancellationTokenSource qui a initialement fourni le jeton. Il y a beaucoup à faire avec les nouveaux mécanismes d'annulation. Vous pouvez en savoir plus sur ici .

Utilisez des poignées d'attente

Cette méthode peut être utile si votre thread de travail nécessite d'attendre un intervalle spécifique ou un signal pendant son fonctionnement normal. Vous pouvez Set a ManualResetEvent, par exemple, pour informer le thread qu'il est temps de s'arrêter. Vous pouvez tester l'événement à l'aide de la fonction WaitOne qui renvoie un bool indiquant si l'événement a été signalé. WaitOne prend un paramètre qui spécifie combien de temps attendre l'appel pour retourner 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 existe d'autres instances WaitHandle sur lesquelles le thread peut devoir attendre. Tu peux appeler WaitHandle.WaitAny pour attendre tout événement (y compris l'événement d'arrêt) en un seul appel. Utiliser un événement peut être mieux que d'appeler Thread.Interrupt puisque vous avez plus de contrôle sur le déroulement du programme (Thread.Interrupt lève une exception, vous devrez donc placer stratégiquement le try-catch blocs pour effectuer tout nettoyage nécessaire).

Scénarios spécialisés

Il existe plusieurs scénarios ponctuels qui ont des mécanismes d'arrêt très spécialisés. Il est certainement hors de la portée de cette réponse de les énumérer tous (sans oublier que ce serait presque impossible). Un bon exemple de ce que je veux dire ici est la classe Socket. Si le thread est bloqué lors d'un appel à Send ou Receive, alors appeler Close interrompra le socket quel que soit l'appel de blocage auquel il se trouve pour le débloquer. Je suis sûr qu'il existe plusieurs autres domaines dans la BCL où des techniques similaires peuvent être utilisées pour débloquer un thread.

Interrompez le thread via Thread.Interrupt

L'avantage ici est qu'il est simple et que vous n'avez pas à vous concentrer vraiment sur votre code. L'inconvénient est que vous avez peu de contrôle sur l'emplacement des points de sécurité dans votre algorithme. La raison en est que Thread.Interrupt fonctionne en injectant une exception dans l'un des appels de blocage BCL prédéfinis. Ceux-ci inclus Thread.Sleep, WaitHandle.WaitOne, Thread.Join, etc. Vous devez donc savoir où vous les placez. Cependant, la plupart du temps l'algorithme dicte où ils vont et c'est généralement bien quand même, surtout si votre algorithme passe la plupart de son temps dans l'un de ces appels de blocage. Si votre algorithme n'utilise pas l'un des appels de blocage dans la BCL, cette méthode ne fonctionnera pas pour vous. La théorie ici est que le ThreadInterruptException est uniquement généré à partir de l'appel en attente .NET, il est donc probablement à un point sûr. À tout le moins, vous savez que le thread ne peut pas être en code non managé ou sortir d'une section critique en laissant un verrou pendant dans un état acquis. Bien que cela soit moins invasif que Thread.Abort Je décourage toujours son utilisation car il n'est pas évident de savoir quels appels y répondent et de nombreux développeurs ne connaissent pas ses nuances.

101
Brian Gideon

Eh bien, malheureusement, dans le multithreading, vous devez souvent compromettre "l'accrochage" pour la propreté ... vous pouvez quitter un thread immédiatement si vous Interrupt, mais ce ne sera pas très propre. Alors non, vous n'avez pas à saupoudrer le _shouldStop vérifie 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 qu'il s'agit peut-être d'un thread qui effectue une opération asynchrone de longue durée ou un type de bloc pour l'opération d'entrée), vous pouvez Interrupt, mais vous devez toujours intercepter le ThreadInterruptedException et quittez le thread proprement. Je pense que les exemples que vous avez lus sont très appropriés.

Update 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);
    }
}
12
Kiril

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 - cela peut être quelque chose que vous devez concevoir.

Supposons que votre thread effectue à tout moment l'une des deux choses.

  1. Quelque chose lié au CPU
  2. En attente du noyau

Si vous êtes lié au CPU dans le thread en question, vous probablement avez un bon endroit pour insérer la vérification de renflouement. Si vous appelez le code de quelqu'un d'autre pour effectuer une tâche longue durée liée au processeur, vous devrez peut-être corriger le code externe, le retirer du processus (abandonner les threads est mauvais, mais abandonner les 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 descripteur correspondant, le noyau reviendra immédiatement avec un code d'échec. Si vous êtes en .net/Java/etc. vous vous retrouverez probablement avec une exception. En C, quel que soit le code que vous avez déjà en place pour gérer les échecs des appels système, propage l'erreur jusqu'à une partie significative de votre application. Quoi qu'il en soit, vous sortez de l'endroit bas assez proprement et en temps opportun sans avoir besoin de nouveau code saupoudré partout.

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

Vous semblez laisser 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 - vous feriez bien mieux d'annuler le travail d'arrière-plan de manière synchrone et de joindre l'ancien fil d'arrière-plan au fil de premier plan. C'est beaucoup plus propre car:

  1. Il évite une classe de conditions de concurrence lorsque les anciens fils bgwork reviennent à la vie après des retards inattendus.

  2. Il évite les fuites potentielles de thread/mémoire cachées 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 que vous pouvez abandonner votre propre code en temps opportun. Si l'annulation est une exigence de votre application, la décision que vous devez vraiment prendre est une décision concernant les ressources/les affaires: faites un piratage ou résolvez votre problème proprement.

  2. Vous ne faites pas confiance au code que vous appelez, car il est hors de votre contrôle. Si vous ne lui faites vraiment pas confiance, envisagez de le retirer du processus. Vous obtenez une bien meilleure isolation de nombreux types de risques, y compris celui-ci, de cette façon.

9
blucz

Peut-être que la partie du problème est que vous avez une boucle méthode/while aussi longue. Que vous rencontriez ou non des problèmes de thread, vous devez le décomposer en étapes de traitement plus petites. 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 boucle indéfiniment, mais vérifie s'il est temps de s'arrêter entre chaque étape métier.

2
Brent Arias

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

Si vous avez un fil droit "allez faire quelque chose et fermez" (pas de boucles dedans), alors vous vérifiez simplement le booléen _shouldStop avant ou après chaque point majeur à l'intérieur du fil. De cette façon, vous savez s'il doit continuer ou renflouer.

par exemple:

public void DoWork() {

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

2
NotMe

La meilleure réponse dépend en grande partie de ce que vous faites dans le fil.

  • Comme vous l'avez dit, la plupart des réponses tournent autour de l'interrogation d'un booléen partagé toutes les deux lignes. Même si vous ne l'aimez peut-être pas, c'est souvent le schéma le plus simple. Si vous voulez vous faciliter la vie, vous pouvez écrire une méthode comme ThrowIfCancelled (), qui lève une sorte d'exception si vous avez terminé. Les puristes diront que c'est (haleter) en utilisant des exceptions pour contrôler le flux, mais là encore, le cacelling est une imo exceptionnelle.

  • Si vous effectuez des opérations IO (comme les tâches réseau), vous pouvez envisager de tout faire en utilisant des opérations asynchrones.

  • Si vous effectuez une séquence d'étapes, vous pouvez utiliser l'astuce IEnumerable pour créer une machine à états. Exemple:

<

abstract class StateMachine : IDisposable
{
    public abstract IEnumerable<object> Main();

    public virtual void Dispose()
    {
        /// ... override with free-ing code ...
   }

   bool wasCancelled;

   public bool Cancel()
   {
     // ... set wasCancelled using locking scheme of choice ...
    }

   public Thread Run()
   {
       var thread = new Thread(() =>
           {
              try
              {
                if(wasCancelled) return;
                foreach(var x in Main())
                {
                    if(wasCancelled) return;
                }
              }
              finally { Dispose(); }
           });
       thread.Start()
   }
}

class MyStateMachine : StateMachine
{
   public override IEnumerabl<object> Main()
   {
       DoSomething();
       yield return null;
       DoSomethingElse();
       yield return null;
   }
}

// then call new MyStateMachine().Run() to run.

>

Une ingénierie excessive? Cela dépend du nombre de machines d'état que vous utilisez. Si vous n'en avez qu'un, oui. Si vous en avez 100, alors peut-être pas. Trop compliqué? En fait ça dépend. Un autre avantage de cette approche est qu'elle vous permet (avec des modifications mineures) de déplacer votre opération dans un rappel Timer.tick et d'annuler le filetage si cela a du sens.

et faites tout ce que dit blucz aussi.

2
Glenn

Au lieu d'ajouter une boucle while où une boucle n'appartient pas autrement, ajoutez quelque chose comme if (_shouldStop) CleanupAndExit(); partout où cela est logique de le faire. Il n'est pas nécessaire de vérifier après chaque opération ou de saupoudrer le code partout avec eux. Au lieu de cela, pensez à chaque vérification comme une chance de quitter le fil à ce stade et ajoutez-les stratégiquement dans cet esprit.

2
Adrian Lopez

Toutes ces réponses SO supposent que le thread de travail va boucler. Cela ne me convient pas

Il n'y a pas beaucoup de façons de rendre le code long. La boucle est une construction de programmation assez essentielle. Faire du code prend beaucoup de temps sans boucle prend une quantité énorme d'instructions. Des centaines de milliers.

Ou appeler un autre code qui fait la boucle pour vous. Oui, difficile de faire arrêter ce code à la demande. Cela ne fonctionne tout simplement pas.

1
Hans Passant