如何在C#中实现一个定时器的任务异步?

8

我希望某个操作能在一定时间内执行。当时间到期后,发送另一个执行命令。

StartDoingStuff();
System.Threading.Thread.Sleep(200);
StopDoingStuff();

如何使用C#中的Async/Task/Await来编写代码,而不是在其中放置一个阻止应用程序其余部分的睡眠语句?


我有点困惑,你问的是关于async-await的问题,这是C# 5.0的新功能,但是你的问题标记为C# 4.0。那么,到底是哪一个版本呢? - svick
已将标签更改以匹配问题。 - Alex
@ElHaix 其他人似乎建议不要抛出异常。抛出异常是正常的,因为代码将更加优雅、可维护,并且是实现任务取消模式的好方法 - "使用ThrowIfCancellationRequested方法。以这种方式取消的任务转换为已取消状态,调用代码可以使用该状态来验证任务是否响应了其取消请求。"想象一下,您有多个使用相同取消令牌的方法。使用异常来简化所有逻辑。 - Jaycee
1
我想在这里重复我的评论。虽然看起来这是一个逻辑上可行的事情,但是对于持续时间为200ms的操作而言,抛出取消异常可能会相对昂贵,尤其是如果这段代码像Start/Stop/Start/Stop一样重复执行。 - avo
当出现问题并经过分析诊断后,应解决性能问题。为了过早地进行优化而使代码变得更加复杂通常是一个坏主意。 - Jaycee
1
@ElHaix,你是想要为可以恢复的任务做这个吗,比如 StartDoingStuff(); Sleep(200); StopDoingStuff(); Sleep(200); ResumeDoingStuff(); /* etc.. */ - noseratio - open to work
3个回答

10

2011年,Joe Hoag在Parallel Team的博客中回答了这个问题:Crafting a Task.TimeoutAfter Method

该解决方案使用了TaskCompletionSource并包含了几个优化(通过避免捕获可实现12%的性能提升),处理了清理和覆盖调用已经完成的目标任务、传递无效超时等边缘情况。

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();
}

3
假设StartDoingStuff和StopDoingStuff已经被创建为异步方法并返回任务,则:
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的异常,以提供任务结束方式的指示,从而避免处理布尔值的需要。

2
为什么异步等待需要这两个方法也是“async”? - svick
我重新阅读了问题并修改了方法。谢谢。 - Jaycee
那个忙等令牌的行为似乎没有任何意义。 - usr
如果你指的是while(true)...那只是一个例子。 - Jaycee
抛出取消异常对于只持续200毫秒的事情来说相对昂贵,特别是如果这段代码像Do/Stop/Do/Stop一样重复执行。 - avo

2
这是我会如何做,使用 任务取消模式(不抛出异常的选项)。 [编辑] 更新为使用 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());
}

1
如果您想在 .Net 4.5 上创建一个在一定时间后自动取消的 CancellationToken,可以使用 一个CancellationTokenSource构造函数重载 - svick
很好的建议 @svick,我已经更新了代码以使用这个功能。 - noseratio - open to work
你应该抛出异常。根据你自己的“任务取消模式”链接:“这样做的首选方法是使用ThrowIfCancellationRequested方法。以这种方式取消的任务会转换为已取消状态”。 - Jaycee
1
@Jaycee,选择权在我手中,因为工作任务完全封装在DoSomethingWithTimeoutAsync中,所以出于简单起见,我选择不使用异常。 - noseratio - open to work

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接