Home > Software engineering >  How can I create a Task that can cancel itself and another Task if needed?
How can I create a Task that can cancel itself and another Task if needed?

Time:05-03

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?

CodePudding user response:

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;

}

CodePudding user response:

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! ---");
}
  • Related