【问题标题】:How to implement Task Async for a timer in C#?如何在 C# 中为计时器实现任务异步?
【发布时间】:2013-09-09 21:32:03
【问题描述】:

我希望给定的操作执行一定的时间。当该时间到期时,发送另一个执行命令。

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

我如何在 C# 中使用 Async/Task/Await 来编写它,而不是在那里有一个阻塞应用程序其余部分的 sleep 语句?

【问题讨论】:

  • 我很困惑,你问async-await,这是C# 5.0 的一个新特性,但是你的问题被标记为C# 4.0。那么,它是哪一个?
  • 切换标签以匹配问题
  • @ElHaix 其他人似乎建议不要抛出异常。抛出异常是正常的,因为代码会更优雅,可维护并且是实现 的好方法Task Cancellation Pattern - “使用 ThrowIfCancellationRequested 方法。以这种方式取消的任务将转换为 Canceled 状态,调用代码可以使用该状态来验证任务是否响应了它的取消请求。”想象一下,您有多个方法使用相同的取消令牌。使用异常来简化所有逻辑。
  • 我会在这里重复我的评论。虽然这看起来是合乎逻辑的事情,但对于持续 200 毫秒的事情来说,抛出取消异常可能相对昂贵,尤其是如果此代码像 Start/Stop/Start/Stop 这样重复。
  • @ElHaix,您是否希望为可以恢复的任务执行此操作,例如 StartDoingStuff(); Sleep(200); StopDoingStuff(); Sleep(200); ResumeDoingStuff(); /* etc.. */

标签: c# .net multithreading async-await


【解决方案1】:

假设 StartDoingStuff 和 StopDoingStuff 已创建为返回 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,提供有关任务如何结束的指示,避免需要弄乱布尔值。

【讨论】:

  • 为什么异步等待也需要async这两个方法?
  • 我已经修改了重新阅读问题的方法。谢谢。
  • 那个忙着等待令牌似乎没有目的。
  • 如果你的意思是while(true)...这是一个插图。
  • 对于只持续 200 毫秒的事情来说,抛出一个取消异常是相对昂贵的,特别是如果这段代码像 Do/Stop/Do/Stop 一样重复...
【解决方案2】:

这是我的做法,使用task cancellation pattern(不引发异常的选项)。

[EDITED] 更新为使用 Svick 的建议通过 CancellationTokenSource constructor 设置超时。

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

【讨论】:

  • 如果你想创建一个CancellationToken在.Net 4.5上一段时间后自动取消,有a CancellationTokenSource constructor overload for that
  • 好点@svick,我已更新代码以使用此功能。
  • 你应该抛出异常。从您自己的“任务取消模式”链接:“执行此操作的首选方法是使用 ThrowIfCancellationRequested 方法。以这种方式取消的任务将转换为 Canceled 状态”
  • @Jaycee,选择权在我,因为工作任务完全封装在 DoSomethingWithTimeoutAsync 中,为了简单起见,我选择不使用异常。
【解决方案3】:

Joe Hoag 在 2011 年的 Parallel 团队博客中回答了这个问题: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();
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-05
    • 1970-01-01
    • 2015-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多