Comment abandonner / annuler des tâches TPL?

Réponses:

228

Vous ne pouvez pas. Les tâches utilisent les threads d'arrière-plan du pool de threads. L'annulation des threads à l'aide de la méthode Abort n'est pas recommandée. Vous pouvez consulter le billet de blog suivant qui explique une manière appropriée d'annuler des tâches à l'aide de jetons d'annulation. Voici un exemple:

class Program
{
    static void Main()
    {
        var ts = new CancellationTokenSource();
        CancellationToken ct = ts.Token;
        Task.Factory.StartNew(() =>
        {
            while (true)
            {
                // do some heavy work here
                Thread.Sleep(100);
                if (ct.IsCancellationRequested)
                {
                    // another thread decided to cancel
                    Console.WriteLine("task canceled");
                    break;
                }
            }
        }, ct);

        // Simulate waiting 3s for the task to complete
        Thread.Sleep(3000);

        // Can't wait anymore => cancel this task 
        ts.Cancel();
        Console.ReadLine();
    }
}
Darin Dimitrov
la source
5
Belle explication. J'ai une question, comment ça marche quand nous n'avons pas de méthode anonyme dans Task.Factory.StartNew? comme Task.Factory.StartNew (() => ProcessMyMethod (),
cancelToken
61
que se passe-t-il s'il y a un appel bloquant qui ne retourne pas dans la tâche en cours d'exécution?
mehmet6parmak
3
@ mehmet6parmak Je pense que la seule chose que vous pouvez faire est de l'utiliser Task.Wait(TimeSpan / int)pour lui donner un délai (basé sur le temps) de l'extérieur.
Mark
2
Et si j'ai ma classe personnalisée pour gérer l'exécution des méthodes dans un nouveau Task? Quelque chose comme: public int StartNewTask(Action method). A l' intérieur du StartNewTaskméthode que je crée un nouveau Taskpar: Task task = new Task(() => method()); task.Start();. Alors, comment puis-je gérer le CancellationToken? Je voudrais également savoir si Threadje dois implémenter une logique pour vérifier s'il y a des tâches qui sont toujours en suspens et donc les tuer quand Form.Closing. Avec Threadsj'utilise Thread.Abort().
Cheshire Cat
Omg, quel mauvais exemple! C'est une simple condition booléenne, bien sûr c'est la première que l'on essaierait! Mais c'est-à-dire que j'ai une fonction dans une tâche séparée, qui peut prendre beaucoup de temps pour se terminer, et idéalement, il ne devrait rien savoir sur un threading ou autre. Alors, comment annuler la fonction avec vos conseils?
Hi-Angel
32

L'annulation d'une tâche est facilement possible si vous capturez le thread dans lequel la tâche s'exécute. Voici un exemple de code pour illustrer ceci:

void Main()
{
    Thread thread = null;

    Task t = Task.Run(() => 
    {
        //Capture the thread
        thread = Thread.CurrentThread;

        //Simulate work (usually from 3rd party code)
        Thread.Sleep(1000);

        //If you comment out thread.Abort(), then this will be displayed
        Console.WriteLine("Task finished!");
    });

    //This is needed in the example to avoid thread being still NULL
    Thread.Sleep(10);

    //Cancel the task by aborting the thread
    thread.Abort();
}

J'ai utilisé Task.Run () pour montrer le cas d'utilisation le plus courant pour cela - en utilisant le confort de Tasks avec l'ancien code à un seul thread, qui n'utilise pas la classe CancellationTokenSource pour déterminer s'il doit être annulé ou non.

Florian Rappl
la source
2
Merci pour cette idée. Utilisé cette approche pour implémenter un délai d'expiration pour un code externe, qui n'a pas de CancellationTokensupport ...
Christoph Fink
7
AFAIK thread.abort vous laissera inconnu sur votre pile, il peut être invalide. Je ne l'ai jamais essayé mais je suppose que démarrer un thread dans un domaine d'application séparé que thread.abort sera enregistré! De plus, un thread entier est gaspillé dans votre solution juste pour abandonner une tâche. Vous n'auriez pas à utiliser des tâches mais des threads en premier lieu. (
vote négatif
1
Comme je l'ai écrit, cette solution est un dernier recours qui pourrait être envisagé dans certaines circonstances. Bien sûr, une CancellationTokensolution, voire plus simple, sans condition de concurrence, devrait être envisagée. Le code ci-dessus illustre uniquement la méthode, pas le domaine d'utilisation.
Florian Rappl
8
Je pense que cette approche pourrait avoir des conséquences inconnues et je ne la recommanderais pas dans le code de production. Les tâches ne sont pas toujours garanties d'être exécutées dans un thread différent, ce qui signifie qu'elles peuvent être exécutées dans le même thread que celui qui les a créées si le planificateur le décide (ce qui signifie que le thread principal sera passé à la threadvariable locale). Dans votre code, vous pourriez alors finir par abandonner le thread principal, ce qui n'est pas ce que vous voulez vraiment. Peut-être qu'un contrôle si les threads sont les mêmes avant l'abandon serait une bonne idée, si vous insistez pour abandonner
Ivaylo Slavov
10
@Martin - Comme j'ai recherché cette question sur SO, je vois que vous avez voté à la baisse plusieurs réponses qui utilisent Thread.Abort pour tuer des tâches, mais vous n'avez fourni aucune solution alternative. Comment supprimer un code tiers qui ne prend pas en charge l'annulation et qui s'exécute dans une tâche ?
Gordon Bean
32

Comme le suggère cet article , cela peut être fait de la manière suivante:

int Foo(CancellationToken token)
{
    Thread t = Thread.CurrentThread;
    using (token.Register(t.Abort))
    {
        // compute-bound work here
    }
}

Bien que cela fonctionne, il n'est pas recommandé d'utiliser une telle approche. Si vous pouvez contrôler le code qui s'exécute dans la tâche, vous feriez mieux de gérer correctement l'annulation.

starteleport
la source
6
+1 pour donner une approche différente tout en indiquant ses replis. Je ne savais pas que cela pouvait être fait :)
Joel
1
Merci pour la solution! Nous pouvons simplement passer le jeton à la méthode et annuler la source de jeton, au lieu d'obtenir d'une manière ou d'une autre l'instance de thread de la méthode et d'abandonner directement cette instance.
Sam
Le thread de la tâche qui est abandonné peut être un thread de pool de threads ou le thread de l'appelant qui appelle la tâche. Pour éviter cela, vous pouvez utiliser un TaskScheduler pour spécifier un thread dédié pour la tâche .
Edward Brey
19

Ce genre de chose est l'une des raisons logistiques pour lesquelles Abortest obsolète. D'abord et avant tout, ne pas utiliser Thread.Abort()pour annuler ou arrêter un thread si cela est possible. Abort()ne doit être utilisé que pour tuer de force un thread qui ne répond pas à des demandes plus pacifiques d'arrêter en temps opportun.

Cela étant dit, vous devez fournir un indicateur d'annulation partagé qu'un thread définit et attend pendant que l'autre thread vérifie périodiquement et se termine normalement. .NET 4 comprend une structure spécialement conçue à cet effet, le CancellationToken.

Adam Robinson
la source
8

Vous ne devriez pas essayer de faire cela directement. Concevez vos tâches pour qu'elles fonctionnent avec un CancellationToken et annulez-les de cette façon.

De plus, je recommanderais de changer votre thread principal pour qu'il fonctionne également via un CancellationToken. L'appel Thread.Abort()est une mauvaise idée - cela peut entraîner divers problèmes qui sont très difficiles à diagnostiquer. Au lieu de cela, ce thread peut utiliser la même annulation que vos tâches - et la même chose CancellationTokenSourcepeut être utilisée pour déclencher l'annulation de toutes vos tâches et de votre thread principal.

Cela conduira à une conception beaucoup plus simple et plus sûre.

Reed Copsey
la source
8

Pour répondre à la question de Prerak K sur la façon d'utiliser CancellationTokens lorsque vous n'utilisez pas une méthode anonyme dans Task.Factory.StartNew (), vous passez le CancellationToken en tant que paramètre dans la méthode que vous démarrez avec StartNew (), comme indiqué dans l'exemple MSDN ici .

par exemple

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

Task.Factory.StartNew( () => DoSomeWork(1, token), token);

static void DoSomeWork(int taskNum, CancellationToken ct)
{
    // Do work here, checking and acting on ct.IsCancellationRequested where applicable, 

}
Jools
la source
7

J'utilise une approche mixte pour annuler une tâche.

  • Tout d'abord, j'essaie de l'annuler poliment en utilisant l' annulation .
  • S'il est toujours en cours d'exécution (par exemple à cause d'une erreur d'un développeur), alors ne vous comportez pas correctement et tuez-le en utilisant une méthode Abort à l' ancienne .

Consultez un exemple ci-dessous:

private CancellationTokenSource taskToken;
private AutoResetEvent awaitReplyOnRequestEvent = new AutoResetEvent(false);

void Main()
{
    // Start a task which is doing nothing but sleeps 1s
    LaunchTaskAsync();
    Thread.Sleep(100);
    // Stop the task
    StopTask();
}

/// <summary>
///     Launch task in a new thread
/// </summary>
void LaunchTaskAsync()
{
    taskToken = new CancellationTokenSource();
    Task.Factory.StartNew(() =>
        {
            try
            {   //Capture the thread
                runningTaskThread = Thread.CurrentThread;
                // Run the task
                if (taskToken.IsCancellationRequested || !awaitReplyOnRequestEvent.WaitOne(10000))
                    return;
                Console.WriteLine("Task finished!");
            }
            catch (Exception exc)
            {
                // Handle exception
            }
        }, taskToken.Token);
}

/// <summary>
///     Stop running task
/// </summary>
void StopTask()
{
    // Attempt to cancel the task politely
    if (taskToken != null)
    {
        if (taskToken.IsCancellationRequested)
            return;
        else
            taskToken.Cancel();
    }

    // Notify a waiting thread that an event has occurred
    if (awaitReplyOnRequestEvent != null)
        awaitReplyOnRequestEvent.Set();

    // If 1 sec later the task is still running, kill it cruelly
    if (runningTaskThread != null)
    {
        try
        {
            runningTaskThread.Join(TimeSpan.FromSeconds(1));
        }
        catch (Exception ex)
        {
            runningTaskThread.Abort();
        }
    }
}
Alex Klaus
la source
4

Les tâches ont un support de première classe pour l'annulation via des jetons d'annulation . Créez vos tâches avec des jetons d'annulation et annulez les tâches via ceux-ci explicitement.

Tim Lloyd
la source
4

Vous pouvez utiliser a CancellationTokenpour contrôler si la tâche est annulée. Parlez-vous de l'abandonner avant qu'il ne démarre ("tant pis, je l'ai déjà fait"), ou de l'interrompre au milieu? Si le premier, le CancellationTokenpeut être utile; dans ce dernier cas, vous devrez probablement implémenter votre propre mécanisme de "renflouement" et vérifier aux points appropriés dans l'exécution de la tâche si vous devez échouer rapidement (vous pouvez toujours utiliser le CancellationToken pour vous aider, mais c'est un peu plus manuel).

MSDN a un article sur l'annulation des tâches: http://msdn.microsoft.com/en-us/library/dd997396.aspx

Hank
la source
3

Les tâches sont en cours d'exécution sur le ThreadPool (au moins, si vous utilisez la fabrique par défaut), donc l'abandon du thread ne peut pas affecter les tâches. Pour abandonner des tâches, consultez Annulation de tâche sur msdn.

Oliver Hanappi
la source
1

J'ai essayé CancellationTokenSourcemais je ne peux pas faire ça. Et je l'ai fait à ma manière. Et il fonctionne.

namespace Blokick.Provider
{
    public class SignalRConnectProvider
    {
        public SignalRConnectProvider()
        {
        }

        public bool IsStopRequested { get; set; } = false; //1-)This is important and default `false`.

        public async Task<string> ConnectTab()
        {
            string messageText = "";
            for (int count = 1; count < 20; count++)
            {
                if (count == 1)
                {
                //Do stuff.
                }

                try
                {
                //Do stuff.
                }
                catch (Exception ex)
                {
                //Do stuff.
                }
                if (IsStopRequested) //3-)This is important. The control of the task stopping request. Must be true and in inside.
                {
                    return messageText = "Task stopped."; //4-) And so return and exit the code and task.
                }
                if (Connected)
                {
                //Do stuff.
                }
                if (count == 19)
                {
                //Do stuff.
                }
            }
            return messageText;
        }
    }
}

Et une autre classe de l'appel de la méthode:

namespace Blokick.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class MessagePerson : ContentPage
    {
        SignalRConnectProvider signalR = new SignalRConnectProvider();

        public MessagePerson()
        {
            InitializeComponent();

            signalR.IsStopRequested = true; // 2-) And this. Make true if running the task and go inside if statement of the IsStopRequested property.

            if (signalR.ChatHubProxy != null)
            {
                 signalR.Disconnect();
            }

            LoadSignalRMessage();
        }
    }
}
Hasan Thon Oruç
la source
0

Vous pouvez abandonner une tâche comme un thread si vous pouvez provoquer la création de la tâche sur son propre thread et appeler Abortson Threadobjet. Par défaut, une tâche s'exécute sur un thread de pool de threads ou sur le thread appelant, que vous ne souhaitez généralement pas abandonner.

Pour vous assurer que la tâche obtient son propre thread, créez un planificateur personnalisé dérivé de TaskScheduler. Dans votre implémentation de QueueTask, créez un nouveau thread et utilisez-le pour exécuter la tâche. Plus tard, vous pouvez abandonner le thread, ce qui entraînera l'exécution de la tâche dans un état défectueux avec un ThreadAbortException.

Utilisez ce planificateur de tâches:

class SingleThreadTaskScheduler : TaskScheduler
{
    public Thread TaskThread { get; private set; }

    protected override void QueueTask(Task task)
    {
        TaskThread = new Thread(() => TryExecuteTask(task));
        TaskThread.Start();
    }

    protected override IEnumerable<Task> GetScheduledTasks() => throw new NotSupportedException(); // Unused
    protected override bool NotSupportedException(Task task, bool taskWasPreviouslyQueued) => throw new NotSupportedException(); // Unused
}

Commencez votre tâche comme ceci:

var scheduler = new SingleThreadTaskScheduler();
var task = Task.Factory.StartNew(action, cancellationToken, TaskCreationOptions.LongRunning, scheduler);

Plus tard, vous pouvez abandonner avec:

scheduler.TaskThread.Abort();

Notez que la mise en garde concernant l'abandon d'un thread s'applique toujours:

La Thread.Abortméthode doit être utilisée avec prudence. En particulier, lorsque vous l'appelez pour abandonner un thread autre que le thread actuel, vous ne savez pas quel code a été exécuté ou n'a pas pu s'exécuter lorsque ThreadAbortException est levée, ni ne pouvez être certain de l'état de votre application ou de tout état d'application et utilisateur qu'il est chargé de préserver. Par exemple, l'appel Thread.Abortpeut empêcher les constructeurs statiques de s'exécuter ou empêcher la libération de ressources non managées.

Edward Brey
la source
Ce code échoue avec une exception d'exécution: System.InvalidOperationException: RunSynchronously ne peut pas être appelé sur une tâche qui a déjà été démarrée.
Theodor Zoulias
1
@TheodorZoulias Bonne prise. Merci. J'ai corrigé le code et généralement amélioré la réponse.
Edward Brey
1
Oui, cela a corrigé le bogue. Une autre mise en garde qu'il convient probablement de mentionner est qu'il Thread.Abortn'est pas pris en charge sur .NET Core. Tenter de l'utiliser là entraîne une exception: System.PlatformNotSupportedException: l'abandon de thread n'est pas pris en charge sur cette plate-forme. Une troisième mise en garde est que le SingleThreadTaskSchedulerne peut pas être utilisé efficacement avec des tâches de style promesse, c'est-à-dire avec des tâches créées avec des asyncdélégués. Par exemple, un incorporé ne await Task.Delay(1000)s'exécute dans aucun thread, il n'est donc pas affecté par les événements de thread.
Theodor Zoulias