如何创建一个任务,它可以在需要时取消自己和另一个任务?



假设我有一个简单的UWP应用程序(所以没有。net 5或c# 8没有与此情况无关的解决方案),有许多包含按钮的页面,所有这些都必须能够通过调用SeriousWorkAsyncFunWorkAsync开始工作:

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);
}
}

我的要求如下:

  • 所有按钮在任何时候都不能被禁用。
  • 任何任务都不能并发运行。
  • 每当我调用SeriousWorkAsync时,我希望FunWorkAsync完成执行,取消完成后,SeriousWorkAsync应该启动。
  • 同样地,如果我调用SeriousWorkAsync而另一个调用SeriousWorkAsync正在执行,我必须取消另一个调用,并且新的调用应该只在取消完成后才做事情。
  • 如果有任何额外的调用,第一个调用应该首先取消,只有最后一个调用应该执行。

到目前为止,我能想到的最佳解决方案是在循环中延迟任务,直到另一个任务被取消,并在方法完成执行后立即设置几个布尔标志:

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;
}
}
然而,这感觉像是一个非常肮脏的解决方法,并且它没有满足我的最后一个需求。我知道我应该使用CancellationToken,但到目前为止,即使经过大量的搜索和头脑风暴,我尝试使用它也没有成功。那么,我该怎么做呢?

经过一番搜索,我发现了"一种自我取消和重新启动任务的模式"。这正是我所需要的,经过一些调整,我可以肯定地说我得到了我想要的。我的实现如下:

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();
}

理想情况下,调用方法应该是这样的:

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

如果你愿意,你可以把你的方法包装在一个try catch中,并总是生成一个新的令牌,这样消费者就不需要为取消应用特殊的处理:

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

最后,我使用SeriousWorkAsyncFunWorkAsync的以下实现进行测试:

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

因为你正在使用任务,你需要等待一个任务完成,你可以使用这个机制来等待下一次执行开始。

我没有测试这个代码,但它应该可以工作。

// 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;
}

最新更新