Como abortar / cancelar tarefas TPL?

Respostas:

228

Você não pode. As tarefas usam threads de segundo plano do pool de threads. Também não é recomendável cancelar threads usando o método Abort. Você pode dar uma olhada na seguinte postagem no blog, que explica uma maneira adequada de cancelar tarefas usando tokens de cancelamento. Aqui está um exemplo:

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
fonte
5
Boa explicação. Eu tenho uma pergunta, como isso funciona quando não temos um método anônimo no Task.Factory.StartNew? Como Task.Factory.StartNew (() => ProcessMyMethod (),
cancelellationToken
61
e se houver uma chamada de bloqueio que não retorne dentro da tarefa de execução?
mehmet6parmak
3
@ mehmet6parmak Acho que a única coisa que você pode fazer é usar Task.Wait(TimeSpan / int)um prazo (com base no tempo) do lado de fora.
Mark
2
E se eu tiver minha classe personalizada para gerenciar a execução de métodos dentro de uma nova Task? Algo como: public int StartNewTask(Action method). Dentro do StartNewTaskmétodo que eu criar uma nova Taskpor: Task task = new Task(() => method()); task.Start();. Então, como posso gerenciar o CancellationToken? Eu também gostaria de saber se Threadpreciso implementar uma lógica para verificar se ainda existem algumas tarefas pendentes e, portanto, matá-las quando Form.Closing. Com Threadseu uso Thread.Abort().
Cheshire Cat
Omg, que exemplo ruim! É uma condição booleana simples, é claro que é a primeira coisa que se tenta! Mas, ou seja, eu tenho uma função em uma Tarefa separada, que pode levar muito tempo para ser finalizada e, idealmente, não deve saber nada sobre um encadeamento ou o que seja. Então, como cancelo a função com o seu conselho?
Hi-Angel
32

A interrupção de uma tarefa é facilmente possível se você capturar o encadeamento no qual a tarefa está sendo executada. Aqui está um código de exemplo para demonstrar isso:

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();
}

Usei Task.Run () para mostrar o caso de uso mais comum para isso - usando o conforto do Tasks com código de thread único antigo, que não usa a classe CancellationTokenSource para determinar se deve ou não ser cancelado.

Florian Rappl
fonte
2
Obrigado por esta ideia. Usado essa abordagem para implementar um tempo limite para algum código externo, que não tem CancellationTokenapoio ...
Christoph Fink
7
O AFAIK thread.abort o deixará desconhecido sobre sua pilha, ela pode ser inválida. Eu nunca tentei, mas acho que iniciar um thread em um domínio de aplicativo separado que thread.abort será salvo! Além disso, um thread inteiro é desperdiçado em sua solução apenas para abortar uma tarefa. Você não precisaria usar tarefas, mas threads em primeiro lugar. (downvote)
Martin Meeser
1
Como escrevi - esta solução é um último recurso que pode ser considerado em determinadas circunstâncias. Obviamente, uma CancellationTokenou mais simples soluções que são livres de condições de corrida devem ser consideradas. O código acima ilustra apenas o método, não a área de uso.
Florian Rappl
8
Penso que esta abordagem pode ter consequências desconhecidas e não a recomendaria no código de produção. Nem sempre é garantido que as tarefas sejam executadas em um encadeamento diferente, o que significa que podem ser executadas no mesmo encadeamento que o criou, se o planejador assim o decidir (o que significa que o encadeamento principal será passado para a threadvariável local). No seu código, você pode acabar cancelando o thread principal, que não é o que você realmente deseja. Talvez uma verificação se os tópicos são os mesmos antes do aborto seria uma boa penso ter, se você insistir em abortar
Ivaylo Slavov
10
@ Martin - Como eu pesquisei essa pergunta no SO, vejo que você votou negativamente em várias respostas que usam o Thread.Abort para matar tarefas, mas você não forneceu soluções alternativas. Como você pode matar código de terceiros que não oferece suporte ao cancelamento e está sendo executado em uma tarefa ?
Gordon Feijão
32

Como este post sugere, isso pode ser feito da seguinte maneira:

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

Embora funcione, não é recomendável usar essa abordagem. Se você pode controlar o código que é executado na tarefa, é melhor seguir o tratamento adequado do cancelamento.

starteleport
fonte
6
+1 por fornecer uma abordagem diferente ao declarar seus fallbacks. Eu não sabia que isso poderia ser feito :)
Joel
1
Obrigado pela solução! Podemos apenas passar o token para o método e cancelar o tokensource, em vez de, de alguma maneira, obter a instância do thread do método e abortar essa instância diretamente.
Sam
O encadeamento da tarefa que é interrompido pode ser um encadeamento de conjunto de encadeamentos ou o encadeamento do chamador que está chamando a tarefa. Para evitar isso, você pode usar um TaskScheduler para especificar um thread dedicado para a tarefa .
Edward Brey
19

Esse tipo de coisa é uma das razões logísticas para a Abortdescontinuação. Em primeiro lugar, não use Thread.Abort()para cancelar ou interromper um encadeamento, se possível. Abort()deve ser usado apenas para eliminar forçosamente um encadeamento que não está respondendo a solicitações mais pacíficas para parar em tempo hábil.

Dito isto, é necessário fornecer um indicador de cancelamento compartilhado de que um segmento define e aguarda enquanto o outro segmento verifica e sai periodicamente normalmente. O .NET 4 inclui uma estrutura projetada especificamente para esse fim, o CancellationToken.

Adam Robinson
fonte
8

Você não deve tentar fazer isso diretamente. Projete suas tarefas para trabalhar com um CancellationToken e cancele-as dessa maneira.

Além disso, eu recomendaria alterar o thread principal para funcionar também por meio de um CancellationToken. Ligar Thread.Abort()é uma má ideia - pode levar a vários problemas que são muito difíceis de diagnosticar. Em vez disso, esse encadeamento pode usar o mesmo cancelamento usado pelas suas tarefas - e o mesmo CancellationTokenSourcepode ser usado para acionar o cancelamento de todas as suas tarefas e do encadeamento principal.

Isso levará a um design muito mais simples e seguro.

Reed Copsey
fonte
8

Para responder à pergunta de Prerak K sobre como usar CancellationTokens quando não estiver usando um método anônimo em Task.Factory.StartNew (), você passa o CancellationToken como um parâmetro para o método que você está iniciando com StartNew (), como mostra o exemplo do MSDN aqui .

por exemplo

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
fonte
7

Eu uso uma abordagem mista para cancelar uma tarefa.

  • Em primeiro lugar, estou tentando cancelá-lo educadamente usando o cancelamento .
  • Se ainda estiver em execução (por exemplo, devido a um erro do desenvolvedor), comporte-se mal e mate-o usando o método Abort da velha escola .

Confira um exemplo abaixo:

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
fonte
4

As tarefas têm suporte de primeira classe para cancelamento por meio de tokens de cancelamento . Crie suas tarefas com tokens de cancelamento e cancele-as explicitamente.

Tim Lloyd
fonte
4

Você pode usar a CancellationTokenpara controlar se a tarefa é cancelada. Você está falando sobre abortá-lo antes de começar ("deixa pra lá, eu já fiz isso") ou realmente interrompe-o no meio? Se o primeiro, o CancellationTokenpode ser útil; neste último caso, você provavelmente precisará implementar seu próprio mecanismo de "resgate" e verificar nos pontos apropriados na execução da tarefa se deve falhar rapidamente (ainda é possível usar o CancellationToken para ajudá-lo, mas é um pouco mais manual).

O MSDN possui um artigo sobre o cancelamento de tarefas: http://msdn.microsoft.com/en-us/library/dd997396.aspx

Hank
fonte
3

As tarefas estão sendo executadas no ThreadPool (pelo menos, se você estiver usando o padrão de fábrica), portanto, o cancelamento do encadeamento não pode afetar as tarefas. Para interromper tarefas, consulte Cancelamento de tarefas no msdn.

Oliver Hanappi
fonte
1

Eu tentei, CancellationTokenSourcemas não posso fazer isso. E eu fiz isso do meu jeito. E isso funciona.

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;
        }
    }
}

E outra classe de chamar o método:

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 Atum Oruç
fonte
0

Você pode abortar uma tarefa como um encadeamento se puder fazer com que a tarefa seja criada em seu próprio encadeamento e chamar AbortseuThread objeto. Por padrão, uma tarefa é executada em um thread do conjunto de encadeamentos ou no encadeamento de chamada - nenhum dos quais você normalmente deseja anular.

Para garantir que a tarefa obtenha seu próprio encadeamento, crie um planejador personalizado derivado TaskScheduler. Na sua implementação de QueueTask, crie um novo encadeamento e use-o para executar a tarefa. Posteriormente, você pode interromper o encadeamento, o que fará com que a tarefa seja concluída em um estado com falha com a ThreadAbortException.

Use este planejador de tarefas:

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
}

Inicie sua tarefa assim:

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

Mais tarde, você pode abortar com:

scheduler.TaskThread.Abort();

Observe que a advertência sobre o cancelamento de um encadeamento ainda se aplica:

O Thread.Abortmétodo deve ser usado com cautela. Particularmente quando você o chama para abortar um segmento que não seja o segmento atual, você não sabe qual código foi executado ou falhou ao ser executado quando o ThreadAbortException é acionado, nem pode ter certeza do estado do seu aplicativo ou de qualquer aplicativo e estado do usuário que é responsável pela preservação. Por exemplo, a chamada Thread.Abortpode impedir a execução de construtores estáticos ou a liberação de recursos não gerenciados.

Edward Brey
fonte
Esse código falha com uma exceção de tempo de execução: System.InvalidOperationException: RunSynchronously não pode ser chamado em uma tarefa que já foi iniciada.
Theodor Zoulias
1
@TheodorZoulias Good catch. Obrigado. Corrigi o código e geralmente melhorei a resposta.
Edward Brey
1
Sim, isso corrigiu o erro. Outra ressalva que provavelmente deve ser mencionada é que Thread.Abortnão há suporte no .NET Core. A tentativa de usá-lo resulta em uma exceção: System.PlatformNotSupportedException: O cancelamento de encadeamento não é suportado nesta plataforma. Uma terceira ressalva é que ela SingleThreadTaskSchedulernão pode ser usada efetivamente com tarefas no estilo de promessa, ou seja, com tarefas criadas com asyncdelegados. Por exemplo, um incorporado await Task.Delay(1000)é executado em nenhum encadeamento, portanto, não é afetado por eventos de encadeamento.
Theodor Zoulias