如何让 Task 和 CancellationToken 达到 Task.WhenAny 的效果?
How to get effect of Task.WhenAny for a Task and CancellationToken?
我有一个交互式任务,在 "worst" 场景中根本没有执行,因此它由 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
...
}
我没有create/manage令牌,所以无法更改。我必须处理它。
Update:对于这种特殊情况,可以在令牌上使用 Register
方法来取消 TaskCompletionSource
。有关更通用的方法,请参阅 Matthew Watson 的回答。
您可以创建一个额外的任务,当取消标记的等待句柄发出信号时 returns:
var factory = new CancellationTokenSource();
var token = factory.Token;
await Task.WhenAny(
Task.Run(() => token.WaitHandle.WaitOne()),
myTask());
(但是,请注意,这虽然简单,但确实会占用一个额外的线程,这显然不理想。稍后请参阅不使用额外线程的替代解决方案。)
如果您想检查哪个任务已完成,您必须在调用 WhenAny()
之前保留一份任务副本,以便您可以将它们与 return 值进行比较,例如:
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);
}
}
}
如果您不想浪费整个线程等待取消令牌发出信号,您可以使用 CancellationToken.Register()
注册一个回调,您可以使用该回调设置 TaskCompletionSource
:
public static Task WhenCanceled(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
cancellationToken.Register(s => ((TaskCompletionSource<bool>) s).SetResult(true), tcs);
return tcs.Task;
}
然后您可以按如下方式使用它:
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);
}
}
}
对于一般情况,这是一种更可取的方法。
这是一个将 CancellationToken
转换为 Task
或 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);
}
}
使用示例。只需 await
任何任务:
await Task.WhenAny(tcs.Task, token.AsTask());
...或 await
并在同一行中得到结果:
var data = await Task.WhenAny(tcs.Task, token.AsTask<UserData>()).Unwrap();
抛出InvalidOperationException
以防万一,确保CancellationToken
的任务永远不会运行完成。它的Status
只能是Created
、Canceled
或Faulted
.
在这种情况下,您必须非常小心泄漏。特别是,让注册到长期 CancellationToken
.
的委托引用的对象
我最终在 AsyncEx library 中采用的方法如下所示:
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);
}
上面的代码确保如果 CancellationToken
没有被取消,注册就会被处理。
用法:
async Task MyCodeAsync(CancellationToken token)
{
UserData data = await userDataTask.WaitAsync(token);
}
我有一个交互式任务,在 "worst" 场景中根本没有执行,因此它由 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
...
}
我没有create/manage令牌,所以无法更改。我必须处理它。
Update:对于这种特殊情况,可以在令牌上使用 Register
方法来取消 TaskCompletionSource
。有关更通用的方法,请参阅 Matthew Watson 的回答。
您可以创建一个额外的任务,当取消标记的等待句柄发出信号时 returns:
var factory = new CancellationTokenSource();
var token = factory.Token;
await Task.WhenAny(
Task.Run(() => token.WaitHandle.WaitOne()),
myTask());
(但是,请注意,这虽然简单,但确实会占用一个额外的线程,这显然不理想。稍后请参阅不使用额外线程的替代解决方案。)
如果您想检查哪个任务已完成,您必须在调用 WhenAny()
之前保留一份任务副本,以便您可以将它们与 return 值进行比较,例如:
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);
}
}
}
如果您不想浪费整个线程等待取消令牌发出信号,您可以使用 CancellationToken.Register()
注册一个回调,您可以使用该回调设置 TaskCompletionSource
:
public static Task WhenCanceled(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
cancellationToken.Register(s => ((TaskCompletionSource<bool>) s).SetResult(true), tcs);
return tcs.Task;
}
然后您可以按如下方式使用它:
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);
}
}
}
对于一般情况,这是一种更可取的方法。
这是一个将 CancellationToken
转换为 Task
或 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);
}
}
使用示例。只需 await
任何任务:
await Task.WhenAny(tcs.Task, token.AsTask());
...或 await
并在同一行中得到结果:
var data = await Task.WhenAny(tcs.Task, token.AsTask<UserData>()).Unwrap();
抛出InvalidOperationException
以防万一,确保CancellationToken
的任务永远不会运行完成。它的Status
只能是Created
、Canceled
或Faulted
.
在这种情况下,您必须非常小心泄漏。特别是,让注册到长期 CancellationToken
.
我最终在 AsyncEx library 中采用的方法如下所示:
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);
}
上面的代码确保如果 CancellationToken
没有被取消,注册就会被处理。
用法:
async Task MyCodeAsync(CancellationToken token)
{
UserData data = await userDataTask.WaitAsync(token);
}