如何创建一个可以自行取消的任务和另一个任务(如果需要)?
How can I create a Task that can cancel itself and another Task if needed?
假设我有一个简单的 UWP 应用程序(所以没有 .NET 5 或 C# 8 没有与这种情况无关的解决方法),有许多包含按钮的页面,所有这些都必须能够通过调用 SeriousWorkAsync
和 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);
}
}
我的要求如下:
- None 个按钮可以随时禁用。
- 任何任务都不应 运行 同时发生。
- 每当我调用
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,但到目前为止我尝试使用它都没有成功,即使经过大量搜索和头脑风暴之后也是如此。那么,我应该怎么做呢?
因为您正在使用任务并且需要等待任务完成,所以您可以使用此机制在下一次执行开始之前等待。
我没有测试这段代码,但它应该可以工作。
// 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;
}
经过大量搜索,我找到了“A pattern for self-cancelling and restarting task”。这正是我所需要的,经过一些调整后,我可以肯定地说我得到了我想要的。我的实现如下:
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 { }
最后,我对 SeriousWorkAsync
和 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! ---");
}
假设我有一个简单的 UWP 应用程序(所以没有 .NET 5 或 C# 8 没有与这种情况无关的解决方法),有许多包含按钮的页面,所有这些都必须能够通过调用 SeriousWorkAsync
和 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);
}
}
我的要求如下:
- None 个按钮可以随时禁用。
- 任何任务都不应 运行 同时发生。
- 每当我调用
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,但到目前为止我尝试使用它都没有成功,即使经过大量搜索和头脑风暴之后也是如此。那么,我应该怎么做呢?
因为您正在使用任务并且需要等待任务完成,所以您可以使用此机制在下一次执行开始之前等待。
我没有测试这段代码,但它应该可以工作。
// 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;
}
经过大量搜索,我找到了“A pattern for self-cancelling and restarting task”。这正是我所需要的,经过一些调整后,我可以肯定地说我得到了我想要的。我的实现如下:
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 { }
最后,我对 SeriousWorkAsync
和 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! ---");
}