1

Let's say I have a simple UWP app (so no .NET 5 or C# 8 without workarounds irrelevant to this situation), with many pages that contain buttons, all of which must be able to start work by calling SeriousWorkAsync and FunWorkAsync:

public async Task SeriousWorkAsync(SeriousObject obj)
{
    Setup(obj);
    for (int i = 0; i < 10000; i++)
    {
        await SeriousThingAsync(i);
    }
}

public async Task FunWorkAsync(FunObject obj)
{
    Setup(obj);
    for (int i = 0; i < 10000; i++)
    {
        await FunnyThingAsync(i);
    }
}

My requirements are as follows:

  • None of the buttons can be disabled at any point.
  • No tasks should ever run concurrently.
  • Whenever I call SeriousWorkAsync, I want FunWorkAsync to finish execution, and after cancellation is complete, SeriousWorkAsync should start.
  • Likewise, if I call SeriousWorkAsync while another call to SeriousWorkAsync is executing, I have to cancel that another call, and the newer call should only do stuff after cancellation is complete.
  • If there's any extra calls, the first call should cancel first, and only the last call should execute.

So far, the best solution I could come up with is delaying the Task in a loop until the other one's cancelled, with a few boolean flags that are set as soon as the method finishes execution:

private bool IsDoingWork = false;
private bool ShouldCancel = false;

public async Task FunWorkAsync(FunObject obj)
{
    CancelPendingWork();
    while (IsDoingWork)
    {
        await Task.Delay(30);
    }

    IsDoingWork = true;
    Setup(obj);
    for (int i = 0; i < 10000; i++)
    {
        if (ShouldCancel)
        {
            break;
        }
        await FunnyThingAsync(i);
    }

    IsDoingWork = false;
}

private void CancelPendingWork()
{
    if (IsDoingWork)
    {
        ShouldCancel = true;
    }
}

However, this feels like a very dirty workaround, and it doesn't address my last requirement. I know I should use CancellationToken, but my attempts at using it have been unsuccessful so far, even after a lot of searching and brainstorming. So, how should I go about this?

  • Check this: `CancellationTokenSource.CreateLinkedTokenSource()` https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.createlinkedtokensource?view=net-6.0#system-threading-cancellationtokensource-createlinkedtokensource(system-threading-cancellationtoken) – Poul Bak Apr 25 '22 at 21:53
  • CreateLinkedTokenSource might be able to work – Ax1le Apr 28 '22 at 07:32
  • Yup, `CreateLinkedTokenSource` was quite helpful. Thank you all. –  May 03 '22 at 14:40

2 Answers2

1

After a lot of searching, I came across "A pattern for self-cancelling and restarting task". This was exactly what I needed, and after some tweaks, I can safely say I got what I wanted. My implementation goes as follows:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// The task that is currently pending.
/// </summary>
private Task _pendingTask = null;

/// <summary>
/// A linked token source to control Task execution.
/// </summary>
private CancellationTokenSource _tokenSource = null;

/// <summary>
/// Does some serious work.
/// </summary>
/// <exception cref="OperationCanceledException">Thrown when the
/// operation is cancelled.</exception>
public async Task SeriousWorkAsync(CancellationToken token)
{
    await CompletePendingAsync(token);
    this._pendingTask = SeriousImpl(this._tokenSource.Token);
    await this._pendingTask;
}

/// <summary>
/// Does some fun work.
/// </summary>
/// <exception cref="OperationCanceledException">Thrown when the
/// operation is cancelled.</exception>
public async Task FunWorkAsync(CancellationToken token)
{
    await CompletePendingAsync(token);
    this._pendingTask = FunImpl(this._tokenSource.Token);
    await this._pendingTask;
}

/// <summary>
/// Cancels the pending Task and waits for it to complete.
/// </summary>
/// <exception cref="OperationCanceledException">If the new token has
/// been canceled before the Task, an exception is thrown.</exception>
private async Task CompletePendingAsync(CancellationToken token)
{
    // Generate a new linked token
    var previousCts = this._tokenSource;
    var newCts = CancellationTokenSource.CreateLinkedTokenSource(token);
    this._tokenSource = newCts;

    if (previousCts != null)
    {
        // Cancel the previous session and wait for its termination
        previousCts.Cancel();
        try { await this._pendingTask; } catch { }
    }

    // We need to check if we've been canceled
    newCts.Token.ThrowIfCancellationRequested();
}

Ideally, calling the methods would look like this:

try
{
    await SeriousWorkAsync(new CancellationToken());
}
catch (OperationCanceledException) { }

If you prefer, you can wrap your methods inside a try catch and always generate a new token, so consumers wouldn't need to apply special handling for cancellation:

var token = new CancellationToken();
try
{
    await CompletePendingAsync(token);
    this._pendingTask = FunImpl(this._tokenSource.Token);
    await this._pendingTask;
}
catch { }

Lastly, I tested using the following implementations for SeriousWorkAsync and FunWorkAsync:

private async Task SeriousImpl(CancellationToken token)
{
    Debug.WriteLine("--- Doing serious stuff ---");
    for (int i = 1000; i <= 4000; i += 1000)
    {
        token.ThrowIfCancellationRequested();
        Debug.WriteLine("Sending mails for " + i + "ms...");
        await Task.Delay(i);
    }
    Debug.WriteLine("--- Done! ---");
}

private async Task FunImpl(CancellationToken token)
{
    Debug.WriteLine("--- Having fun! ---");
    for (int i = 1000; i <= 4000; i += 1000)
    {
        token.ThrowIfCancellationRequested();
        Debug.WriteLine("Laughing for " + i + "ms...");
        await Task.Delay(i);
    }
    Debug.WriteLine("--- Done! ---");
}
  • An improved version of this pattern can be found [here](https://gist.github.com/LowProfileKitty/3fcc5151930bd4ae277a53652d876691). It's much easier to use and helps if you have complex cancellation logic. –  May 03 '22 at 15:21
0

Because you are using tasks and you need to wait for a Task to complete you can use this mechanism to wait before your next execution starts.

I did not test this code but it should work.

// Store current task for later
private Task CurrentTask = null;
// Create new cancellation token for cancelling the task
private CancellationTokenSource TokenSource = new CancellationTokenSource();
private object WorkLock = new object();

public async Task FunWorkAsync(FunObject obj)
{
    // Define the task we will be doing
    var task = new Task(async () =>
    {
        Setup(obj);
        for (int i = 0; i < 10000; i++)
        {
            // Break from the task when requested
            if (TokenSource.IsCancellationRequested)
            {
                break;
            }
            await FunnyThingAsync(i);
        }
    });
    
    // Make sure that we do not start multiple tasks at once
    lock (WorkLock)
    {
        if (CurrentTask != null)
        {
            TokenSource.Cancel();
            // You should make sure here that you can continue by providing cancellation token with a timeout
            CurrentTask.Wait(CancellationToken.None);
        }
        CurrentTask = task;
        // Restart cancelation token for new task
        TokenSource = new CancellationTokenSource();
        task.Start();
    }
    await task;

}
Tomasz Juszczak
  • 2,276
  • 15
  • 25
  • Sadly, this didn't work, tasks are still able to run concurrently. Your use of lock made me think of whether Tasks have something similar available to them, and turns out they do, with the [Semaphore](https://learn.microsoft.com/en-us/dotnet/api/system.threading.semaphore) –  Apr 27 '22 at 01:21