我希望给定的操作执行一定的时间。当该时间到期时,发送另一个执行命令。
StartDoingStuff();
System.Threading.Thread.Sleep(200);
StopDoingStuff();
我如何在 C# 中使用 Async/Task/Await 来编写它,而不是在那里有一个阻塞应用程序的其余部分的 sleep 语句?
我希望给定的操作执行一定的时间。当该时间到期时,发送另一个执行命令。
StartDoingStuff();
System.Threading.Thread.Sleep(200);
StopDoingStuff();
我如何在 C# 中使用 Async/Task/Await 来编写它,而不是在那里有一个阻塞应用程序的其余部分的 sleep 语句?
Joe Hoag 在 2011 年的 Parallel Team 博客中回答了这个问题:Crafting a Task.TimeoutAfter Method。
该解决方案使用 TaskCompletionSource 并包括多项优化(仅通过避免捕获 12%)、处理清理并涵盖边缘情况,例如在目标任务已经完成时调用 TimeoutAfter、传递无效超时等。
Task.TimeoutAfter 的美妙之处在于它很容易与其他延续组合起来,因为它只做一件事情:通知您超时已过期。它不会尝试取消您的任务。当抛出 TimeoutException 时,您可以决定要做什么。
还介绍了 Stephen Toub使用的快速实现async/await
,但也没有涵盖边缘情况。
优化后的实现是:
public static Task TimeoutAfter(this Task task, int millisecondsTimeout)
{
// Short-circuit #1: infinite timeout or task already completed
if (task.IsCompleted || (millisecondsTimeout == Timeout.Infinite))
{
// Either the task has already completed or timeout will never occur.
// No proxy necessary.
return task;
}
// tcs.Task will be returned as a proxy to the caller
TaskCompletionSource<VoidTypeStruct> tcs =
new TaskCompletionSource<VoidTypeStruct>();
// Short-circuit #2: zero timeout
if (millisecondsTimeout == 0)
{
// We've already timed out.
tcs.SetException(new TimeoutException());
return tcs.Task;
}
// Set up a timer to complete after the specified timeout period
Timer timer = new Timer(state =>
{
// Recover your state information
var myTcs = (TaskCompletionSource<VoidTypeStruct>)state;
// Fault our proxy with a TimeoutException
myTcs.TrySetException(new TimeoutException());
}, tcs, millisecondsTimeout, Timeout.Infinite);
// Wire up the logic for what happens when source task completes
task.ContinueWith((antecedent, state) =>
{
// Recover our state data
var tuple =
(Tuple<Timer, TaskCompletionSource<VoidTypeStruct>>)state;
// Cancel the Timer
tuple.Item1.Dispose();
// Marshal results to proxy
MarshalTaskResults(antecedent, tuple.Item2);
},
Tuple.Create(timer, tcs),
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
return tcs.Task;
}
和 Stephen Toub 的实现,没有检查边缘情况:
public static async Task TimeoutAfter(this Task task, int millisecondsTimeout)
{
if (task == await Task.WhenAny(task, Task.Delay(millisecondsTimeout)))
await task;
else
throw new TimeoutException();
}
假设 StartDoingStuff 和 StopDoingStuff 已创建为 Async 方法返回 Task 然后
await StartDoingStuff();
await Task.Delay(200);
await StopDoingStuff();
编辑: 如果原始提问者想要一个在特定时间后取消的异步方法:假设该方法不会发出任何网络请求,而只是在内存中进行一些处理,并且可以在不考虑其影响的情况下任意中止结果,然后使用取消令牌:
private async Task Go()
{
CancellationTokenSource source = new CancellationTokenSource();
source.CancelAfter(200);
await Task.Run(() => DoIt(source.Token));
}
private void DoIt(CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested();
}
}
编辑:我应该提到你可以捕捉到产生的 OperationCanceledException 提供关于任务如何结束的指示,避免需要弄乱布尔值。
这是我的做法,使用任务取消模式(不引发异常的选项)。
[已编辑]更新为使用 Svick 的建议通过CancellationTokenSource
构造函数设置超时。
// return true if the job has been done, false if cancelled
async Task<bool> DoSomethingWithTimeoutAsync(int timeout)
{
var tokenSource = new CancellationTokenSource(timeout);
CancellationToken ct = tokenSource.Token;
var doSomethingTask = Task<bool>.Factory.StartNew(() =>
{
Int64 c = 0; // count cycles
bool moreToDo = true;
while (moreToDo)
{
if (ct.IsCancellationRequested)
return false;
// Do some useful work here: counting
Debug.WriteLine(c++);
if (c > 100000)
moreToDo = false; // done counting
}
return true;
}, tokenSource.Token);
return await doSomethingTask;
}
以下是从异步方法调用它的方法:
private async void Form1_Load(object sender, EventArgs e)
{
bool result = await DoSomethingWithTimeoutAsync(3000);
MessageBox.Show("DoSomethingWithTimeout done:" + result); // false if cancelled
}
以下是如何从常规方法调用它并异步处理完成:
private void Form1_Load(object sender, EventArgs e)
{
Task<bool> task = DoSomethingWithTimeoutAsync(3000);
task.ContinueWith(_ =>
{
MessageBox.Show("DoSomethingWithTimeout done:" + task.Result); // false is cancelled
}, TaskScheduler.FromCurrentSynchronizationContext());
}