如何获得 Task.WhenAny 对 Task 和 CancellationToken 的影响?

ast*_*ker 5 c# asynchronous task async-await cancellation-token

我有交互式任务,在“最坏”场景中根本不执行,因此它由TaskCompletionSource.

我想等待此任务完成,或者我收到的令牌被取消——以先发生者为准。这种工作的完美工具将是Task.WhenAny,唯一的问题是它只需要任务,而我有一个Task和一个CancellationToken

如何等待(异步地,像Task.WhenAny)触发的第一个事件——完成的任务或取消的令牌?

async Task MyCodeAsync(CancellationToken token)
{
  var tcs = new TaskCompletionSource<UserData>(); // represents interactive part

  await Task.WhenAny(tcs.Task, token); // imaginary call

  UserData data = tcs.Task.Result; // user interacted, let's continue
  ...
}
Run Code Online (Sandbox Code Playgroud)

我不创建/管理令牌,所以我无法更改它。我必须处理它。

更新:对于这种特殊情况,可以使用Register令牌上的方法取消TaskCompletionSource. 有关更通用的方法,请参阅 Matthew Watson 的回答。

Mat*_*son 5

您可以创建一个额外的任务,当取消令牌的等待句柄发出信号时返回:

var factory = new CancellationTokenSource();
var token   = factory.Token;

await Task.WhenAny(
    Task.Run(() => token.WaitHandle.WaitOne()),
    myTask());
Run Code Online (Sandbox Code Playgroud)

(但是,请注意,这虽然很简单,但确实会占用一个额外的线程,这显然并不理想。稍后查看不使用额外线程的替代解决方案。)

如果要检查哪个任务已完成,则必须在调用之前保留任务的副本,WhenAny()以便可以将它们与返回值进行比较,例如:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static async Task Main()
        {
            var factory = new CancellationTokenSource(1000); // Change to 3000 for different result.
            var token   = factory.Token;
            var task    = myTask();

            var result = await Task.WhenAny(
                Task.Run(() => token.WaitHandle.WaitOne()),
                task);

            if (result == task)
                Console.WriteLine("myTask() completed");
            else
                Console.WriteLine("cancel token was signalled");
        }

        static async Task myTask()
        {
            await Task.Delay(2000);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您不想浪费整个线程等待发出取消令牌的信号,您可以使用CancellationToken.Register()注册一个回调,您可以使用该回调设置 a 的结果TaskCompletionSource

从这里抬起

public static Task WhenCanceled(CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<bool>();
    cancellationToken.Register(s => ((TaskCompletionSource<bool>) s).SetResult(true), tcs);
    return tcs.Task;
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以按如下方式使用它:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static async Task Main()
        {
            var factory = new CancellationTokenSource(1000);
            var token   = factory.Token;
            var task    = myTask();

            var result = await Task.WhenAny(
                WhenCanceled(token),
                task);

            if (result == task)
                Console.WriteLine("myTask() completed");
            else
                Console.WriteLine("cancel token was signalled");
        }

        public static Task WhenCanceled(CancellationToken cancellationToken)
        {
            var tcs = new TaskCompletionSource<bool>();
            cancellationToken.Register(s => ((TaskCompletionSource<bool>) s).SetResult(true), tcs);
            return tcs.Task;
        }

        static async Task myTask()
        {
            await Task.Delay(2000);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

对于一般情况,这是一种更可取的方法。

  • @TheodorZoulias 我添加了一个不使用额外线程的版本。 (2认同)
  • @astrowalker 对于这种特定用途,您实际上不需要处理 `Register()` 的返回值,所以没关系。(当然,“Task”对象本身也是一样的——它们也是一次性的,但你很少看到它们被处置。 (2认同)

Ste*_*ary 5

在这种情况下,您必须非常小心泄漏。特别是,由注册到长期存在的委托引用的对象CancellationToken

我最终在AsyncEx 库中采用的方法如下所示:

public static async Task<T> WaitAsync<T>(this Task<T> task, CancellationToken token)
{
  var tcs = new TaskCompletionSource<T>();
  using (token.Register(() => tcs.TrySetCanceled(token), useSynchronizationContext: false)
    return await await Task.WhenAny(task, tcs.Task).ConfigureAwait(false);
}
Run Code Online (Sandbox Code Playgroud)

上面的代码可确保在CancellationToken未取消注册的情况下处理注册。

用法:

async Task MyCodeAsync(CancellationToken token)
{
  UserData data = await userDataTask.WaitAsync(token);
}
Run Code Online (Sandbox Code Playgroud)

  • @TheodorZoulias:不。内部的“await”可能会异步运行,因此它需要“ConfigureAwait(false)”。结果值是一个*已经完成的任务*,因此在外部“await”上添加“ConfigureAwait(false)”不会有任何效果。 (2认同)

The*_*ias 4

这是一个将 a 转换CancellationToken为 a Taskor的扩展方法Task<TResult>CancellationToken收到取消请求后,返回的任务将立即以取消状态完成。

static class CancellationTokenExtensions
{
    public static Task AsTask(this CancellationToken token)
    {
        return new Task(() => throw new InvalidOperationException(), token);
    }

    public static Task<TResult> AsTask<TResult>(this CancellationToken token)
    {
        return new Task<TResult>(() => throw new InvalidOperationException(), token);
    }
}
Run Code Online (Sandbox Code Playgroud)

使用示例。只是await任何任务:

await Task.WhenAny(tcs.Task, token.AsTask());
Run Code Online (Sandbox Code Playgroud)

...或者await也在同一行中得到结果:

var data = await Task.WhenAny(tcs.Task, token.AsTask<UserData>()).Unwrap();
Run Code Online (Sandbox Code Playgroud)

抛出InvalidOperationException以防万一,以确保 的任务CancellationToken永远不会运行完成。其Status只能是Created,CanceledFaulted。在 .NET 7 上,异常可能更有意义UnreachableException