异步方法中不等待任务的考虑事项

14

我正在开发一个Web API项目,使用Azure的托管缓存服务将数据库结果缓存在内存中,以提高响应时间并减轻对数据库的重复访问。当尝试将新项放入缓存时,偶尔会抛出一个特定于缓存的异常,代码为DataCacheErrorCode.RetryLater。为了在稍后重试而不需要阻塞此方法,我将其改为async并使用await Task.Delay在短时间后再次尝试。之前,开发人员在其中硬编码了一个Thread.Sleep,这实际上损害了应用程序的性能。

现在,该方法的签名看起来类似于以下内容:

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

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

  

因为此调用未被等待,当前方法的执行在调用完成之前继续。考虑将“await”运算符应用于调用结果。

在这种情况下,由于Put没有返回任何内容,让这个操作fire-and-forget似乎对我有意义,因为我看不到阻止调用它的方法的执行的任何理由。我只是想知道允许许多这些fire-and-forget的Task在后台运行是否存在任何危险或陷阱,因为Put可能经常被调用。或者,无论如何都应该等待,因为99%的时间我不会得到重试错误,而Task几乎立即完成。我只想确保我没有因拥有太多线程(或类似的东西)而遭受任何惩罚。

4个回答

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

想确保没有因线程过多等原因而产生任何罚款。

我只是为了澄清一下。 当您使用Task.Delay时,您并没有创建任何新线程。 一个Task并不总是等同于创建一个新线程。 在这里特别提到,Task.Delay内部使用了一个Timer,因此没有任何线程开销(除非您使用了await,那么当前正在延迟的线程除外)。

2
@alexm 它使用线程池线程在时间到期后短暂地运行处理程序。它在等待时不会消耗线程池线程。这意味着只有在有生产性工作要做时才使用线程,这正是您想要的(也无法避免)。 - Servy
1
@Servy:我再次同意你的说法,这就是为什么我加了一个“技术上”的词语 :) - alexm
2
我不鼓励在ASP.NET上使用await Task.Run(..)来伪造async处理程序,这是绝大多数尝试使用Task.Run的情况。唯一真正的Run用例是“fire-and-forget”。而且很容易发现,99%的“fire-and-forget” ASP.NET场景都不应该使用“fire-and-forget”。更新缓存是极少数可以接受“fire-and-forget”(和Task.Run)的用例之一。关于请求上下文,我的意思是该请求的AspNetSynchronizationContext,当您使用async时,默认情况下会捕获它(正如我在我的博客中所描述的)。 - Stephen Cleary
1
@StephenCleary 如果你只是想忽略上下文,使用ConfiguareAwait(false)是否比使用Task.Run更可取? - Yuval Itzchakov
2
@YuvalItzchakov: 大致相等。类似于 await Task.Yield().ConfigureAwait(false) 将会暂停当前线程,并在线程池线程上恢复方法;而 Task.Run 则将其委托排队到线程池中。我认为 Task.Run 有更清晰的意图(代码与上下文分离),但 ConfigureAwait 可能更有效率。 - Stephen Cleary
显示剩余7条评论

3

这个警告是在告诉你,在某些情况下,你可能并不想要“发射并忘记”的行为。如果你真的想要“发射并忘记”,并且没有问题继续执行而不知道操作何时完成,甚至是否成功完成,那么你可以安全地忽略此警告。


@alexm async void 在 Web API 中使用可能会出现问题,我以前曾收到异常提示,表明不允许这样使用。 - Jesse Carter
2
@alexm 这会使得该方法仅可能通过“烧掉然后忘记”方式被调用,这和允许该方法被等待是不同的,而只是在某些情况下为某些调用者执行“烧掉然后忘记”的语义。 - Servy
@Jesse Carter:你说得对,我只是在建议如何消除警告。不过,调用异步任务方法而不等待它也会产生同样的效果。 - alexm
@alexm 嗯,为了那些会破坏我的应用程序的异常而放弃编译器警告似乎在这一点上并不理想 :P - Jesse Carter
@Jesse Carter:恐怕你在两种情况下都必须处理异常。 - alexm
显示剩余6条评论

2
释放未等待的任务可能带来一个负面影响,那就是编译器本身会发出警告——75个编译器警告本身就是个问题,而且它们还会掩盖真正的警告。
如果你想向编译器表明你有意地不使用任务的结果,你可以使用一个简单的扩展方法,它什么也不做,但满足编译器对明确性的要求。
// Do nothing.
public static void Release(this Task task)
{
}

现在您可以调用。
UpdateCacheAsync(data).Release();

没有任何编译器警告。 https://gist.github.com/lisardggY/396aaca7b70da1bbc4d1640e262e990a

2
从C# 7开始,您可以通过将任务分配给丢弃变量来明确丢弃任务:_ = UpdateCacheAsync(data); - DBN

0

推荐的 ASP.NET 方法是:

HostingEnvironment.QueueBackgroundWorkItem(WorkItem);

...

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

顺便提一下,在非 ASP.NET 线程上未捕获/重新抛出异常可能会导致服务器进程崩溃/重启


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