【问题标题】:Considerations for not awaiting a Task in an asynchronous method在异步方法中不等待任务的注意事项
【发布时间】:2014-11-17 17:28:34
【问题描述】:

我正在开发一个 Web API 项目,该项目使用 Azure 的托管缓存服务将数据库结果缓存到内存中,以缩短响应时间并减少数据库的重复流量。尝试将新项目放入缓存时,有时会抛出特定于缓存的异常,代码为DataCacheErrorCode.RetryLater。自然,为了稍后重试而无需阻止此方法,我将其设置为 asyncawait Task.Delay 稍后再试一次。以前,开发人员在其中硬编码了 Thread.Sleep,这确实会损害应用程序的性能。

方法签名现在看起来与此类似:

public static async Task Put(string cacheKey, object obj)

在此更改之后,我从应用程序中调用以前同步版本的 Put 的所有其他位置收到约 75 条编译器警告,指示:

由于不等待此调用,因此在调用完成之前继续执行当前方法。考虑将 'await' 运算符应用于调用结果。

在这种情况下,由于 Put 没有返回任何内容,因此让此操作 即发即弃 对我来说是有意义的,因为我看不出有任何理由阻止执行调用它的方法。我只是想知道是否有任何危险或陷阱允许大量这些即发即弃的Tasks 在后台运行,因为Put 可以经常被调用。或者我应该等待,因为 99% 的时间我不会收到重试错误,Task 几乎会立即完成。我只是想确保我不会因为线程过多(或类似的东西)而受到任何惩罚。

【问题讨论】:

    标签: c# .net async-await task-parallel-library fire-and-forget


    【解决方案1】:

    如果Put 有可能出于任何原因抛出任何其他异常,并且您每次将对象插入缓存时都不要使用await Put,则异常将被吞没在返回Task 没有被等待。如果您使用的是 .NET 4.0,则此异常将在该 Task. 的终结器中重新抛出。如果您使用的是 .NET 4.5,它将被简单地忽略(这可能是不可取的)。

    想确保我不会因为拥有太多而受到任何惩罚 许多线程或类似的东西。

    我这么说只是为了说明问题。当您使用Task.Delay 时,您不会旋转任何新线程。 Task 并不总是等于正在旋转的新线程。具体来说,Task.Delay 内部使用了Timer,因此没有任何线程开销(如果您确实使用await,则当前延迟的线程除外)。

    【讨论】:

    • @alexm 在时间过去后很短的时间内使用线程池线程来运行处理程序。它在等待时不会消耗线程池线程。这意味着只有在有生产性工作要做时才会使用线程,这正是您想要的(并且无法避免)。
    • @Servy:再次,我同意你的说法,这就是为什么我写了一个“技术上”的词:)
    • 我不鼓励 await Task.Run(..) 在 ASP.NET 上伪造 async 处理程序,这是尝试使用 Task.Run 的绝大多数。唯一真正的Run 用例是即发即弃。并且很容易 99% 的即发即弃 ASP.NET 场景应该使用即发即弃。更新缓存是极少数可以接受即发即弃(和Task.Run)的用例之一。关于请求上下文,我指的是该请求的AspNetSynchronizationContext,即captured by default when you use async(正如我在博客中所描述的那样)。
    • @StephenCleary 如果您只想忽略上下文,难道不使用ConfiguareAwait(false) 比使用Task.Run 更可取吗?
    • @YuvalItzchakov:大致相当。 await Task.Yield().ConfigureAwait(false) 之类的东西将产生当前线程并在线程池线程上恢复该方法;而Task.Run 会将其委托排队到线程池。我认为Task.Run 有更清晰的意图(没有上下文的代码是分开的),但ConfigureAwait 可能更有效。
    【解决方案2】:

    警告是告诉您,您在一个您可能实际上不想开火并忘记的位置发生了火灾并忘记了行为。如果您确实想触发并忘记,并且在不知道操作何时完成或什至成功完成的情况下继续没有问题,那么您可以放心地忽略警告。

    【讨论】:

    • + 1;要将方法标记为显式“即发即弃”,可以将返回类型从 async Task 更改为 async void。
    • @alexm async void 不适用于 Web API 我过去收到过异常,表明我尝试使用它时不允许使用它
    • @alexm 这样一来,该方法可能被调用的唯一方法就是通过火和忘记,这与 允许 等待方法不同,但是只是在某些情况下为某些调用者执行触发并忘记语义。
    • @Jesse Carter:你是对的,我只是建议如何摆脱警告。虽然调用异步任务方法而不等待它具有相同的效果。
    • @alexm 很好地权衡编译器警告以换取会破坏我的应用程序的异常,此时似乎并不可取:P
    【解决方案3】:

    释放任务不等待运行的一个负面结果是编译器警告本身 - 75 个编译器警告本身就是一个问题,它们隐藏了真正的警告。

    如果您想向编译器发出信号,表明您有意不对任务的结果做任何事情,您可以使用一个简单的扩展方法,它什么都不做,但满足编译器对明确性的需求.

    // Do nothing.
    public static void Release(this Task task)
    {
    }
    

    现在你可以打电话了

    UpdateCacheAsync(data).Release();
    

    没有任何编译器警告。

    https://gist.github.com/lisardggY/396aaca7b70da1bbc4d1640e262e990a

    【讨论】:

    • 从 C# 7 开始,您可以通过将任务分配给丢弃变量来显式丢弃任务:_ = UpdateCacheAsync(data);
    【解决方案4】:

    推荐的ASP.NET方式是

    HostingEnvironment.QueueBackgroundWorkItem(WorkItem);
    
    ...
    
    async Task WorkItem(CancellationToken cancellationToken)
    {
        try { await ...} catch (Exception e) { ... }
    }
    

    顺便说一句,在 ASP.NET 线程之外的线程上不捕获/重新抛出可能会导致服务器进程崩溃/重新启动

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-02-23
      • 2014-09-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-22
      • 2014-03-18
      • 1970-01-01
      相关资源
      最近更新 更多