4

I've created a little demo project to help me understand how I can use Cancellation Tokens. I understand that you cancel a token and check if a cancellation has been requested, but is there a way that I can check whether the cancellation has been realised? In my example below, I don't want to run Work() again until DoWork() has finished running.

public class Program
{
    public static CancellationTokenSource tokenSource;

    private static void Main(string[] args)
    {
        while (true)
        {
            Work();
        }
    }

    public static async void Work()
    {
        tokenSource = new CancellationTokenSource();
        Console.WriteLine("Press any key to START doing work");
        Console.ReadLine();
        Console.WriteLine("Press any key to STOP doing work");
        DoWork(tokenSource.Token);
        Console.ReadLine();
        Console.WriteLine("Stopping...");
        tokenSource.Cancel();
    }

    public static async void DoWork(CancellationToken cancelToken)
    {
        while (true)
        {
            Console.WriteLine("Working...");
            await Task.Run(() =>
            {
                Thread.Sleep(1500);
            });

            if (cancelToken.IsCancellationRequested)
            {
                Console.WriteLine("Work Cancelled!");

                return;
            }
        }
    }
}
David Andrew Thorpe
  • 838
  • 1
  • 9
  • 23
  • 1
    Don't use `async void`. Use `async Task`. The resulting `Task` can be awaited (throwing an exception if it's been cancelled) or explicitly checked for completion as you please. – Jeroen Mostert Oct 08 '19 at 11:36

2 Answers2

6

You typically don't want to make your DoWork function async void -- make it async Task instead. That way you can see when it's completed (or been cancelled).

You also probably want to use cancelToken.ThrowIfCancellationRequested(). This throws an OperationCanceledException, which you can catch.

public class Program
{
    public static CancellationTokenSource tokenSource;

    private static async Task Main(string[] args)
    {
        while (true)
        {
            await Work();
        }
    }

    public static async Task Work()
    {
        tokenSource = new CancellationTokenSource();
        Console.WriteLine("Press any key to START doing work");
        Console.ReadLine();
        Console.WriteLine("Press any key to STOP doing work");

        var task = DoWork(tokenSource.Token);

        Console.ReadLine();
        Console.WriteLine("Stopping...");
        tokenSource.Cancel();

        try
        {
            await task;
        }
        catch (OperationCanceledException)
        {
            // Task was cancelled
        }
    }

    public static async Task DoWork(CancellationToken cancelToken)
    {
        while (true)
        {
            Console.WriteLine("Working...");
            await Task.Run(() =>
            {
                Thread.Sleep(1500);
            });

            cancelToken.ThrowIfCancellationRequested();
        }
    }
}

This code relies on "async main", which was introduced in C# 7. If you don't have this, you can write your Main method as:

private static void Main(string[] args)
{
    while (true)
    {
        Work().Wait();
    }
}
canton7
  • 37,633
  • 3
  • 64
  • 77
  • Why just using CancellationToken.IsCancellationRequested property? And also why are there 2 infinitive loops? – is_oz Oct 08 '19 at 11:48
  • 2
    @is_oz the looping structure comes from the OP's question - that's not my code. For `IsCancellationRequested` vs `ThrowIfCancellationRequested`, it doesn't make much difference to this toy example, but it lets the caller tell the difference between "operation completed successfully" and "operation was cancelled". Since the OP is playing around with cancellation, this is a useful distinction to learn. – canton7 Oct 08 '19 at 11:49
  • @canton7 I appreciate the points you raised, however the example you've given only runs the DoWork() method when I'm ready to cancel it. I'm trying to create a demo app where I can press enter to cancel it when I want, and then repeat the process of starting the work again and so on... – David Andrew Thorpe Oct 08 '19 at 11:53
  • @DavidAndrewThorpe It starts the work at exactly the same time that the code in your question starts it, so I'm afraid you need to clarify your complaint. – canton7 Oct 08 '19 at 11:54
  • @canton7 I seem to have a problem with the code, it doesn't wait at 'await task' so Work() is called again before I see the line "Work Cancelled" – David Andrew Thorpe Oct 08 '19 at 12:03
  • 1
    It works fine here. What makes you think it doesn't wait at `await task`? – canton7 Oct 08 '19 at 12:05
  • I've tried the code and it doesn't wait. I've changed 'await task' to 'task.Wait()' and stopped throwing an exception and it works as intended. – David Andrew Thorpe Oct 08 '19 at 12:10
  • @canton7 reading this implies that it won't wait at 'await task' https://stackoverflow.com/questions/15149811/how-to-wait-for-async-method-to-complete – David Andrew Thorpe Oct 08 '19 at 12:11
  • 1
    `task.Wait()` should also throw an exception -- but that one's an `AggregateException` which won't be caught – canton7 Oct 08 '19 at 12:11
  • 2
    What about that question makes you think that `await task` won't await the task? That's literally the entire point of the `await` keyword. – canton7 Oct 08 '19 at 12:12
  • @DavidAndrewThorpe I've updated the code to take `async` all the way up to `Main` (although you'll need to compile as C# 7 or higher). See if that makes a difference. – canton7 Oct 08 '19 at 12:16
  • 1
    @canton7 The answer with 206 votes implies that awaiting the task would not wait for it to complete. Then again I've just copied your code entirely and ran it, and it's working as you said. I made a mistake somewhere in my code so apologies!! And thank you again for the answer – David Andrew Thorpe Oct 08 '19 at 12:17
  • 1
    @DavidAndrewThorpe it doesn't block the thread until the `Task` completes, but it *does* halt execution of that method. That's the entire point of async/await. – canton7 Oct 08 '19 at 12:18
  • @canton7 Ahhh yes I misunderstood – David Andrew Thorpe Oct 08 '19 at 12:19
4

You will make the most out of a CancellationToken if all your operations are asynchronous and cancellable. This way cancelling the token will have immediate effect. You won't have to wait for a Thread.Sleep or other blocking call to complete.

public static async Task DoWork(CancellationToken cancellationToken)
{
    while (true)
    {
        await Console.Out.WriteLineAsync("Working...");
        await Task.Delay(1500, cancellationToken);
    }
}

In this example the token is passed only to Task.Delay, because the WriteLineAsync is not cancellable on .NET Framework (it is on .NET Core).

An OperationCanceledException will be raised by Task.Delay when the token is cancelled.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104