【问题标题】:Synchronous implementation of interface that returns Task返回Task的接口的同步实现
【发布时间】:2015-03-27 16:28:28
【问题描述】:

类似于Implementing an interface that requires a Task return type in synchronous code,尽管我很好奇是否应该忽略我的情况产生的编译器错误。

假设我有一个这样的界面:

public interface IAmAwesome {
    Task MakeAwesomeAsync();
}

在某些实现中,使用 asyncawait 异步完成会带来惊人的好处。这确实是界面试图允许的。

在其他情况下,也许很少见,只需要一个简单的同步方法就可以了。所以让我们假设实现如下所示:

public class SimplyAwesome : IAmAwesome {
    public async Task MakeAwesomeAsync() {
        // Some really awesome stuff here...
    }
}

这可行,但编译器发出警告:

此方法缺少“等待”运算符,将同步运行。 考虑使用 await 运算符来等待非阻塞 API 调用, 或 'await TaskEx.Run(...)' 在后台执行 CPU 密集型工作 线程。

编译器实际上是在建议这个解决方案:

public class SimplyAwesome : IAmAwesome {
    public async Task MakeAwesomeAsync() {
        await Task.Run(() => {
            // Some really awesome stuff here, but on a BACKGROUND THREAD! WOW!
        });
    }
}

我的问题是 - 当我选择忽略此编译器警告时应该确定什么?在某些情况下,工作是如此简单,以至于为它生成一个线程无疑会适得其反。

【问题讨论】:

  • 你“应该”做的是拥有public interface IAmAwesome { Task MakeAwesomeAsync(); void MakeAwesome(); },所以异步和同步方法都暴露了。但如果这对你的现实世界情况是可行的,那就另当别论了。
  • 我想问题是你为什么要做不涉及任何异步的任务。你能提供一个具体的用例吗?
  • @JLRishe 一个常见的例子是做一些有时可以被缓存的事情。 “从缓存中获取”版本可以是同步的,缓存未命中版本是异步的。
  • 在不过度参与的情况下,在我的例子中,示例是定价。有时它是简单的数学运算(即受 CPU 限制),有时它涉及昂贵的 API 和数据库调用。
  • 我会衡量计算的成本。如果它相当快,将结果包装在 Task 中应该没问题。

标签: c# multithreading asynchronous async-await task-parallel-library


【解决方案1】:

正如其他人已经指出的那样,您有 3 种不同的选择,所以这是一个见仁见智的问题:

  1. 保持async,忽略警告
  2. 有同步/async重载
  3. 删除 async 并返回已完成的任务。

我建议返回 already completed task:

public class SimplyAwesome : IAmAwesome 
{
    public Task MakeAwesomeAsync() 
    {
        // run synchronously
        return TaskExtensions.CompletedTask;
    }
}

有几个原因:

  • 由于编译器添加了 try-catch 块,创建一个方法 async 在创建状态机和禁用某些优化(如内联)方面有轻微开销。
  • 您需要处理警告,并且任何其他查看此代码的团队成员都想知道await 在哪里。
  • 在标记完全同步的方法async 时有些不一致。这就像在你的代码中添加while(false){},它的作用是一样的,但它并没有传达方法的含义。

Servy 指出,返回一个任务会改变异常处理的语义。虽然这是真的,但我认为这不是问题。

首先,大部分async代码调用一个方法,并在同一个地方(即await MakeAwesomeAsync())等待返回的任务,这意味着无论该方法是不是async,都会在同一个地方抛出异常与否。

其次,即使是 .Net 框架的任务返回方法也会同步抛出异常。以Task.Delay 哪个throws an exception directly without storing it in the returned task 为例,所以不需要await 任务引发异常:

try
{
    Task.Delay(-2);
}
catch (Exception e)
{
    Console.WriteLine(e);
}

由于 .Net 开发人员需要在 .Net 中同步遇到异常时除外,因此他们也应该从您的代码中除外。

【讨论】:

  • 我不确定为什么这个答案没有更多的赞成票。 IMO,对于不异步运行的方法,不使用 async 关键字是最有意义的。 API 中 Task 的返回类型仅仅意味着方法可以异步运行。这不能保证它会。该方法的实现者可以在不影响公共 API 的情况下做出该实现细节决定。
  • 另外,两个大拇指用 TaskExtensions.CompletedTask 击败 .NET 4.6,它比我一直做的 (Task.FromResult(0) )。
【解决方案2】:

什么时候我选择忽略这个编译器警告? 在某些情况下,工作是如此简单,以至于为其生成一个线程 不可否认会适得其反。

编译器没有说“在这个方法中使用Task.Run”。它只是告诉你你为他准备了一个async 方法,在你的方法声明中添加了async 修饰符,但你实际上并没有等待任何东西。

您可以采取三种方法:

A.您可以忽略编译器警告,一切仍将执行。请注意,状态机生成会有一点开销,但您的方法调用将同步执行。如果操作很耗时并且可能导致方法执行进行阻塞调用,这可能会使使用该方法的用户感到困惑。

B.将“Awesome”的生成分为同步接口和异步接口:

public interface MakeAwesomeAsync
{
    Task MakeAwesomeAsync();
}

public interface MakeAwesome
{
    void MakeAwesome();
}

C.如果该操作不太耗时,您可以使用Task.FromResult 将其简单地包装在Task 中。在选择这个之前,我肯定会测量运行 CPU 绑定操作需要多长时间。

【讨论】:

  • 如果不能拆分界面,你同意Servy的回答吗?
  • 如果该操作不是 CPU 密集型操作,将其包装在 Task.Result 中应该没问题。
  • This will probably confuse the consumers of your code. 仅当操作长时间运行时才如此。如果操作可以非常快速地完成,那么这不是问题。例如,文件 IO 的Stream 实现同步执行其大部分“异步”方法,因为它只需要很少的时间,不值得麻烦异步执行它们,但网络 IO 实现需要异步执行操作,因为他们只需要足够长的时间。
  • @YuvalItzchakov 您需要捕获所有异常,然后使用该异常返回错误任务。如果不这样做,异常将传播到方法的调用者,而不是在结果 Task 中返回。
  • @YuvalItzchakov 这里的状态机开销实际上给了你一些东西;你正在使用它。但是,是的,您需要与其他了解您为什么要编写这样的方法的程序员一起工作。消费者不需要关心,但其他开发者需要关心。
【解决方案3】:

如果您确实想同步执行工作,您知道您的async 方法将始终同步运行,这在这种情况下是可取的,那么请务必忽略警告。如果您理解警告告诉您的内容并认为它所描述的操作是正确的,那么这不是问题。毕竟这是警告而不是错误是有原因的。

当然,另一种选择是不使用async 方法,而是简单地使用Task.FromResult 来返回一个已经完成的任务。它会改变错误处理语义(除非您还捕获所有异常并将它们包装到您返回的任务中)所以至少要注意这一点。如果您确实希望通过生成的 Task 传播异常,则可能值得离开方法 async 并仅取消警告。

【讨论】:

  • 如果我忽略了警告,并且客户端在不知情的情况下使用await 调用该方法,那么任何异常会发生什么?
  • @Yuck 返回的Task 将是Faulted 并将包含有关异常的信息。当等待 Task 时,将重新抛出该异常。它看起来与操作实际上是异步的没有什么不同。
  • 为什么不保留async,而是通过await Task.FromResult 或现在的await Task.CompletedTask 来消除编译器警告?这难道不会实现摆脱编译器警告保持消费者期望的异常处理语义吗?
  • @rory.ap 我看不出如何故意将结果包装在已完成的任务中,然后再次打开它,这比不这样做更好地表达了代码的意图。我提到的第二个选项是其中唯一一个真正表达了同步运行该方法并返回已完成任务的明确意图的选项。
猜你喜欢
  • 2014-12-08
  • 2015-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-01
  • 1970-01-01
  • 2019-12-17
相关资源
最近更新 更多