web-dev-qa-db-fra.com

Est-il possible d'interrompre une tâche comme l'abandon d'un thread (méthode Thread.Abort)?

Nous pourrions abandonner un fil comme celui-ci:

Thread thread = new Thread(SomeMethod);
.
.
.
thread.Abort();

Mais puis-je abandonner une tâche (dans .Net 4.0) de la même manière et non par un mécanisme d'annulation. Je veux tuer la tâche immédiatement.

56
Amir Karimi
  1. Vous ne devez pas utiliser Thread.Abort ()
  2. Les tâches peuvent être annulées mais pas abandonnées.

La méthode Thread.Abort () est (sévèrement) déconseillée.

Les threads et les tâches doivent coopérer lorsqu'ils sont arrêtés, sinon vous courez le risque de laisser le système dans un état instable/non défini.

Si vous devez exécuter un processus et le tuer de l'extérieur, la seule option sûre consiste à l'exécuter dans un AppDomain distinct.

29
Henk Holterman

Les conseils sur la non-utilisation d'un abandon de thread sont controversés. Je pense qu'il y a encore une place pour cela mais dans des circonstances exceptionnelles. Cependant, vous devez toujours essayer de concevoir autour de lui et le voir en dernier recours.

Exemple;

Vous disposez d'une application Windows Form simple qui se connecte à un service Web synchrone bloquant. Dans lequel il exécute une fonction sur le service Web dans une boucle parallèle.

CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

Parallel.ForEach(iListOfItems, po, (item, loopState) =>
{

    Thread.Sleep(120000); // pretend web service call

});

Disons que dans cet exemple, l'appel de blocage prend 2 minutes pour se terminer. Maintenant, je définis mon MaxDegreeOfParallelism pour dire ProcessorCount. iListOfItems contient 1 000 éléments à traiter.

L'utilisateur clique sur le bouton de processus et la boucle commence, nous avons "jusqu'à" 20 threads s'exécutant sur 1000 éléments dans la collection iListOfItems. Chaque itération s'exécute sur son propre thread. Chaque thread utilisera un thread de premier plan lors de sa création par Parallel.ForEach. Cela signifie que, quel que soit l'arrêt de l'application principale, le domaine de l'application sera conservé jusqu'à ce que tous les threads soient terminés.

Cependant, l'utilisateur doit fermer l'application pour une raison quelconque, disons qu'il ferme le formulaire. Ces 20 threads continueront de s'exécuter jusqu'à ce que les 1000 éléments soient traités. Ce n'est pas idéal dans ce scénario, car l'application ne se fermera pas comme le souhaite l'utilisateur et continuera à s'exécuter en arrière-plan, comme on peut le voir en jetant un œil dans le gestionnaire de tâches.

Supposons que l'utilisateur essaie de reconstruire à nouveau l'application (VS 2010), il signale que l'exe est verrouillé, puis il devrait aller dans le gestionnaire de tâches pour le tuer ou simplement attendre que les 1000 éléments soient traités.

Je ne vous blâmerais pas de le dire, mais bien sûr! Je devrais annuler ces threads en utilisant l'objet CancellationTokenSource et appeler Cancel ... mais il y a quelques problèmes avec cela à partir de .net 4.0. Tout d'abord, cela n'aboutira jamais à un abandon de thread qui offrirait une exception d'abandon suivie d'une terminaison de thread, de sorte que le domaine de l'application devra plutôt attendre que les threads se terminent normalement, ce qui signifie attendre le dernier appel de blocage, qui serait la toute dernière itération en cours d'exécution (thread) qui finira par appeler po.CancellationToken.ThrowIfCancellationRequested. Dans l'exemple, cela signifierait que le domaine d'application pourrait toujours rester en vie pendant 2 minutes, même si le formulaire a été fermé et que l'annulation a été appelée.

Notez que l'appel d'annulation sur CancellationTokenSource ne lève pas d'exception sur le (s) thread (s) de traitement, ce qui aurait pour effet d'interrompre l'appel de blocage similaire à un abandon de thread et d'arrêter l'exécution. Une exception est mise en cache prête pour quand tous les autres threads (itérations simultanées) finissent et reviennent, l'exception est levée dans le thread initiateur (où la boucle est déclarée).

J'ai choisi pas pour utiliser l'option Cancel sur un objet CancellationTokenSource. C'est du gaspillage et viole sans doute l'anti-patten bien connu de contrôler le flux du code par des exceptions.

Au lieu de cela, il est sans doute `` préférable '' d'implémenter une simple propriété thread-safe, c'est-à-dire Bool stopExecuting. Ensuite, dans la boucle, vérifiez la valeur de stopExecuting et si la valeur est définie sur true par l'influence externe, nous pouvons prendre un chemin alternatif pour fermer correctement. Puisque nous ne devons pas appeler annuler, cela empêche de vérifier CancellationTokenSource.IsCancellationRequested qui serait autrement une autre option.

Quelque chose comme ce qui suit si la condition serait appropriée dans la boucle;

if (loopState.ShouldExitCurrentIteration || loopState.IsExceptional || stopExecuting) {loopState.Stop (); revenir;}

L'itération se terminera désormais de manière `` contrôlée '' et mettra fin à d'autres itérations, mais comme je l'ai dit, cela ne fait pas grand-chose pour notre problème d'avoir à attendre les appels de longue durée et de blocage qui sont effectués à chaque itération ( thread de boucle parallèle), car ceux-ci doivent se terminer avant que chaque thread puisse avoir la possibilité de vérifier s'il doit s'arrêter.

En résumé, lorsque l'utilisateur ferme le formulaire, les 20 threads seront signalés pour s'arrêter via stopExecuting, mais ils ne s'arrêteront que lorsqu'ils auront terminé l'exécution de leur appel de fonction de longue durée.

Nous ne pouvons rien faire contre le fait que le domaine d'application restera toujours en vie et ne sera libéré que lorsque tous les threads de premier plan seront terminés. Et cela signifie qu'il y aura un délai associé à l'attente de la fin des appels de blocage effectués dans la boucle.

Seul un véritable abandon de thread peut interrompre l'appel de blocage, et vous devez éviter de laisser le système dans un état instable/non défini du mieux que vous pouvez dans le gestionnaire d'exceptions du thread abandonné, ce qui va sans aucun doute. Il appartient au programmeur de décider si cela est approprié, en fonction des poignées de ressources qu'il a choisi de maintenir et de la facilité avec laquelle il est possible de les fermer dans le bloc finalement d'un thread. Vous pouvez vous inscrire avec un jeton pour résilier en cas d'annulation en tant que demi-solution, c'est-à-dire.

CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

Parallel.ForEach(iListOfItems, po, (item, loopState) =>
{

    using (cts.Token.Register(Thread.CurrentThread.Abort))
    {
        Try
        {
           Thread.Sleep(120000); // pretend web service call          
        }
        Catch(ThreadAbortException ex)
        {
           // log etc.
        }
        Finally
        {
          // clean up here
        }
    }

});

mais cela entraînera toujours une exception dans le thread déclarant.

Tout bien considéré, l'interruption des appels bloquants à l'aide des constructions parallel.loop aurait pu être une méthode sur les options, évitant l'utilisation de parties plus obscures de la bibliothèque. Mais pourquoi il n'y a pas d'option pour annuler et éviter de lever une exception dans la méthode de déclaration me semble être un oubli possible.

41
Microsoft Developer

Mais puis-je abandonner une tâche (dans .Net 4.0) de la même manière et non par un mécanisme d'annulation. je veux tuer la tâche immédiatement.

D'autres répondeurs vous ont dit de ne pas le faire. Mais oui, vous pouvez le faire. Vous pouvez fournir Thread.Abort() en tant que délégué à appeler par le mécanisme d'annulation de la tâche. Voici comment vous pouvez configurer ceci:

class HardAborter
{
  public bool WasAborted { get; private set; }
  private CancellationTokenSource Canceller { get; set; }
  private Task<object> Worker { get; set; }

  public void Start(Func<object> DoFunc)
  {
    WasAborted = false;

    // start a task with a means to do a hard abort (unsafe!)
    Canceller = new CancellationTokenSource();

    Worker = Task.Factory.StartNew(() => 
      {
        try
        {
          // specify this thread's Abort() as the cancel delegate
          using (Canceller.Token.Register(Thread.CurrentThread.Abort))
          {
            return DoFunc();
          }
        }
        catch (ThreadAbortException)
        {
          WasAborted = true;
          return false;
        }
      }, Canceller.Token);
  }

  public void Abort()
  {
    Canceller.Cancel();
  }

}

clause de non-responsabilité: ne faites pas cela.

Voici un exemple de ce qu'il ne faut pas faire:

 var doNotDoThis = new HardAborter();

 // start a thread writing to the console
 doNotDoThis.Start(() =>
    {
       while (true)
       {
          Thread.Sleep(100);
          Console.Write(".");
       }
       return null;
    });


 // wait a second to see some output and show the WasAborted value as false
 Thread.Sleep(1000);
 Console.WriteLine("WasAborted: " + doNotDoThis.WasAborted);

 // wait another second, abort, and print the time
 Thread.Sleep(1000);
 doNotDoThis.Abort();
 Console.WriteLine("Abort triggered at " + DateTime.Now);

 // wait until the abort finishes and print the time
 while (!doNotDoThis.WasAborted) { Thread.CurrentThread.Join(0); }
 Console.WriteLine("WasAborted: " + doNotDoThis.WasAborted + " at " + DateTime.Now);

 Console.ReadKey();

output from sample code

38
jltrem

Tout le monde sait (espérons-le) qu'il est mauvais de terminer le fil. Le problème est lorsque vous ne possédez pas un morceau de code que vous appelez. Si ce code s'exécute dans une boucle infinie do/while, appelant lui-même des fonctions natives, etc., vous êtes fondamentalement bloqué. Lorsque cela se produit dans votre propre terminaison de code, arrêter ou supprimer un appel, il est plutôt normal de commencer à tirer sur les méchants (afin de ne pas devenir vous-même un méchant).

Donc, pour ce que ça vaut, j'ai écrit ces deux fonctions de blocage qui utilisent leur propre thread natif, pas un thread du pool ou un thread créé par le CLR. Ils arrêteront le thread si un timeout se produit:

// returns true if the call went to completion successfully, false otherwise
public static bool RunWithAbort(this Action action, int milliseconds) => RunWithAbort(action, new TimeSpan(0, 0, 0, 0, milliseconds));
public static bool RunWithAbort(this Action action, TimeSpan delay)
{
    if (action == null)
        throw new ArgumentNullException(nameof(action));

    var source = new CancellationTokenSource(delay);
    var success = false;
    var handle = IntPtr.Zero;
    var fn = new Action(() =>
    {
        using (source.Token.Register(() => TerminateThread(handle, 0)))
        {
            action();
            success = true;
        }
    });

    handle = CreateThread(IntPtr.Zero, IntPtr.Zero, fn, IntPtr.Zero, 0, out var id);
    WaitForSingleObject(handle, 100 + (int)delay.TotalMilliseconds);
    CloseHandle(handle);
    return success;
}

// returns what's the function should return if the call went to completion successfully, default(T) otherwise
public static T RunWithAbort<T>(this Func<T> func, int milliseconds) => RunWithAbort(func, new TimeSpan(0, 0, 0, 0, milliseconds));
public static T RunWithAbort<T>(this Func<T> func, TimeSpan delay)
{
    if (func == null)
        throw new ArgumentNullException(nameof(func));

    var source = new CancellationTokenSource(delay);
    var item = default(T);
    var handle = IntPtr.Zero;
    var fn = new Action(() =>
    {
        using (source.Token.Register(() => TerminateThread(handle, 0)))
        {
            item = func();
        }
    });

    handle = CreateThread(IntPtr.Zero, IntPtr.Zero, fn, IntPtr.Zero, 0, out var id);
    WaitForSingleObject(handle, 100 + (int)delay.TotalMilliseconds);
    CloseHandle(handle);
    return item;
}

[DllImport("kernel32")]
private static extern bool TerminateThread(IntPtr hThread, int dwExitCode);

[DllImport("kernel32")]
private static extern IntPtr CreateThread(IntPtr lpThreadAttributes, IntPtr dwStackSize, Delegate lpStartAddress, IntPtr lpParameter, int dwCreationFlags, out int lpThreadId);

[DllImport("kernel32")]
private static extern bool CloseHandle(IntPtr hObject);

[DllImport("kernel32")]
private static extern int WaitForSingleObject(IntPtr hHandle, int dwMilliseconds);
3
Simon Mourier

Bien qu'il soit possible d'interrompre un thread, dans la pratique, c'est presque toujours une très mauvaise idée de le faire. Aborting un thread signifie que le thread n'a pas la possibilité de nettoyer après lui-même, laissant les ressources non effacées et les choses dans des états inconnus.

En pratique, si vous abandonnez un thread, vous ne devez le faire qu'en conjonction avec la suppression du processus. Malheureusement, trop de gens pensent que ThreadAbort est un moyen viable d'arrêter quelque chose et de continuer, ce n'est pas le cas.

Étant donné que les tâches s'exécutent en tant que threads, vous pouvez appeler ThreadAbort dessus, mais comme pour les threads génériques, vous ne voulez presque jamais le faire, sauf en dernier recours.

2
Erik Funkenbusch