在做同步工作以满足接口时返回 Task.FromResult<value> 不好吗?

Is returning Task.FromResult<value> when doing synchronous work to satisfy interface bad?

假设我有一个接口 IBackgroundTask:

public interface IBackgroundTask
{
    Task<TaskResult> Execute();
}

使用它的应用程序从队列中读取消息,然后启动消费者逻辑为消息执行后台任务。

接口规定它应该作为 Task 实现,但有时消费者实际上并不需要执行任何异步工作。在这种情况下,我们必须实现的签名最好是 TaskResult Execute();。 但是我们不能改变这个接口实现,它作为实现同步和异步工作的基础。

对于异步工作,这很有效,我们可以适当地使用 async 关键字和 await 异步调用(例如:调用 API,磁盘 IO):

public async Task<TaskResult> Execute()
{
    await PossiblyLongExternalAPICall();
    return new TaskResult();
}

但是对于只做同步工作的消费者(例如:对数据库的查询),我必须像这样实现它:

public Task<TaskResult> Execute()
{
    ExecuteSyncCode();
    return Task.FromResult(new TaskResult()); // is this the right thing to do?
}

对于这种情况,这被认为是正确的方法吗?我已经阅读了有关此主题的文章,看起来 Task.FromResult 对于同步代码来说并不是一个坏习惯,但我想知道是否有更好的选择来应对这种情况。

是的,你做对了。使用 Task.FromResult 方法是创建完整 Task<TResult>.

最经济的方法

如果您预计 IBackgroundTask.Execute 方法的实现将 return 大部分时间完成任务,您可以考虑更改接口,以便 return 类型是 ValueTask<TaskResult> instead of Task<TaskResult>. Creating a completed ValueTask<TResult> imposes no additional allocations, beyond what memory needs to be allocated for the TResult itself. On the contrary a completed Task<TResult> requires ~70 additional bytes per instance. This optimization makes sense if you also anticipate that the Execute method will be invoked in hot paths. Otherwise, if it's invoked once in a while, any performance gains will be absolutely tiny and negligible. The ValueTask<TResult> type is generally Task<TResult>,所以谨慎行事。


注意:此答案假设您决定为具有异步契约的方法提供同步实现是基于充分的理由。一般来说,我不提倡打破 the asynchronous contract。如果您没有充分的理由破坏它,请不要破坏它。

Is this considered the right approach for situations like this?

出于相当微妙的原因,我会说不。

当然,

Task.FromResult 允许您从非 async 方法 return 任务。它适用于成功案例。但是对于特殊情况它并没有像预期的那样工作。

具有异步签名的方法应将所有方法结果放在 returned 任务上。它们可以同步完成——这没有任何问题——但从这个意义上讲,任何异常都被视为“结果”,应该放在 returned 任务中。

因此,如果有 ExecuteSyncCode(甚至 new TaskResult())和 return Task.FromException,您会希望捕获任何异常。然后为了适当迂腐,您可能还想明确地捕获 OperationCanceledException 和 return Task.FromCanceled。当您处理完所有极端情况时,您的代码最终看起来是 something like this.

到那时,您的“简单”同步实现充满了一堆样板代码,这些样板代码混淆了代码并让未来的维护者感到困惑。

出于这个原因,我倾向于只将方法标记为 async 而不是使用 await。这将导致编译器警告 (CS1998 This async method lacks 'await' operators and will run synchronously.),这通常很有用,但您可以 #pragma ignore 用于此特定情况,因为您 想要 同步实现。使用 async 本质上使编译器为您完成所有样板文件,同时保持实现同步。

如果您最终得到多个异步接口的同步实现,我建议您定义几个静态辅助方法 like this。这样,丑陋的 #pragma 就被限制在一个助手 class.

具体答案 - 使用异步代码进行数据库工作

But for a consumer that only does synchronous work (examples: a query to the database),

使用异步代码进行数据库工作。这是 async/await 的主要用例之一。您正在进行的调用没有异步版本的可能性很小。

一般答案 - 避免 Task.FromResult

除了模拟时测试的明显例外,我不建议使用 Task.FromResult,因为可能存在细微的错误:

  • 假装异步方法是同步执行的;他们不 'let go' 执行线程,随后可能导致:
    • 线程匮乏:您的网站将无法处理与使用真正的异步时一样多的请求,
    • 代码 Task.WhenAll 等将不会按预期运行,因为调用者做出了不正确的假设(界面说它是 async),
  • 异常处理(请参阅 Stephen Cleary 的回答);