16

I have a time consuming task which I need to run in a separate thread to avoid locking the GUI thread. As this task progresses, it updates a specific GUI control.

The catch is that the user might move to another part of the GUI before the task is over, and in that case, I have to:

  1. Cancel the ongoing task (if it is active)
  2. Wait till it's done cancelling: this is crucial, because the time consuming task's objective is to update a specific control. If more than one thread tries to do it at once, things might get messy.
  3. Launch the task from scratch

For a concrete example, imagine the form has two parts: one where you navigate a directory tree, and another where you display thumbnails. When the user navigates to another directory, thumbnails need to be refreshed.

First I thought of using a BackgroundWorker and an AutoResetEvent to wait for cancellation, but I must have messed something because I got deadlocked when cancelling. Then I read about TPL, which is supposed to replace BGW and more primitive mechanisms.

Can this be done easily using TPL?

leppie
  • 115,091
  • 17
  • 196
  • 297
dario_ramos
  • 7,118
  • 9
  • 61
  • 108

1 Answers1

29

A few things to note:

  • You can get a CancellationToken from a CancellationTokenSource

  • Task cancellation is a cooperative action: if your task does not periodically check the CancellationToken.IsCancellationRequested property, it doesn't matter how many times you try to cancel the task, it will merrily churn away.

Those things said, here's the general idea:

void Main()
{
    var tokenSource = new CancellationTokenSource();
    var myTask = Task.Factory
        .StartNew(() => DoWork(tokenSource.Token), tokenSource.Token);

    Thread.Sleep(1000);

    // ok, let's cancel it (well, let's "request it be cancelled")
    tokenSource.Cancel();

    // wait for the task to "finish"
    myTask.Wait();
}

public void DoWork(CancellationToken token)
{
    while(!token.IsCancellationRequested)
    {
        // Do useful stuff here
        Console.WriteLine("Working!");
        Thread.Sleep(100);
    }
}
JerKimball
  • 16,584
  • 3
  • 43
  • 55
  • Is it okay to create the token source just once and pass its token to `StartNew` every time? – dario_ramos Feb 15 '13 at 12:49
  • Think of it like a kid's two cans on a string (well, one to N cans: you have one end and all the started tasks geta can. You shout "cancel" in one end, all those sharing the token are gonna hear it. – JerKimball Feb 15 '13 at 15:27
  • 1
    @dario_ramos I believe the question in your comment was answered in a comment [here](http://stackoverflow.com/questions/14896248/deadlock-when-waiting-for-task-to-finish), but for completeness' sake, the answer is no - `CancellationTokenSource` cannot be reset and cancelled again; if you request cancellation on one then you must replace it with a new one if you want to cancel again. – anton.burger Feb 16 '13 at 12:05
  • I strongly recommend to return `Task` from `DoWork` and to replace `Thread.Sleep(100);` with `await Task.Delay(100);` You can just run `DoWork(...)` then, without `Task.Factory.StartNew(...)` – bohdan_trotsenko Nov 10 '15 at 08:08
  • 1
    @bohdan_trotsenko the example uses a non-async method called synchronously. In a synchronous method the right way to wait is `Thread.Sleep`. – Shimmy Weitzhandler Jan 04 '19 at 00:00
  • @JerKimball Is there a way to run a new async `Task` upon cancellation? – Shimmy Weitzhandler Jan 04 '19 at 00:01
  • @shimmy the sync method will consume TPL workers. That's an unpleasant side-effect. – bohdan_trotsenko Jan 09 '19 at 12:12
  • 4
    Beware. Task.Wait will throw an AggregateException indicating that the task was cancelled (which you already know, because you cancelled it). https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.wait?view=netframework-4.8#System_Threading_Tasks_Task_Wait_System_TimeSpan_ – Anthony Hayward Jul 03 '19 at 10:52
  • @AnthonyHayward I'm seeing that problem now. Is the solution just to remove the token from `Task.Factory.StartNew()`? – Brondahl Dec 06 '19 at 17:51
  • @ReinstateMonica--Brondahl-- If you are cancelling your entire task from outside, letting the AggregateException propagate is probably appropriate. Your task didn't succeed. If you are just cancelling the sleep (Task.Wait) and continuing with the rest of your task, you can catch the exception and continue. Removing the cancellation token will remove the ability to cancel the task in the proper way. – Anthony Hayward Dec 09 '19 at 09:13
  • Though a great answer please note that exiting gracefully via `IsCancellationRequested` instead of a cancelled state via `ThrowIfCancellationRequested` can be dangerous. [See Eamon Nerbonne's top comment on this answer for more information](https://stackoverflow.com/a/7343338/585968) –  Jun 06 '21 at 01:31