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 wantFunWorkAsync
to finish execution, and after cancellation is complete,SeriousWorkAsync
should start. - Likewise, if I call
SeriousWorkAsync
while another call toSeriousWorkAsync
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! ---");
}