如何创建一个可以自行取消的任务和另一个任务(如果需要)?

How can I create a Task that can cancel itself and another Task if needed?

假设我有一个简单的 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);
    }
}

我的要求如下:

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

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 { }

最后,我对 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! ---");
}