【问题标题】:How to implement interface method that returns Task<T>?如何实现返回Task<T>的接口方法?
【发布时间】:2015-01-01 07:31:11
【问题描述】:

我有一个界面

interface IFoo
{
  Task<Bar> CreateBarAsync();
}

创建Bar有两种方法,一种是异步的,一种是同步的。我想为这两种方法分别提供一个接口实现。

对于异步方法,实现可能如下所示:

class Foo1 : IFoo
{
  async Task<Bar> CreateBarAsync()
  {
    return await AsynchronousBarCreatorAsync();
  }
}

但是我应该如何实现使用同步方法创建Bar的类Foo2?

我可以实现同步运行的方法:

  async Task<Bar> CreateBarAsync()
  {
    return SynchronousBarCreator();
  }

然后编译器将警告不要在方法签名中使用async:

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

或者,我可以实现显式返回Task&lt;Bar&gt;的方法。在我看来,代码会看起来不那么可读:

  Task<Bar> CreateBarAsync()
  {
    return Task.Run(() => SynchronousBarCreator());
  }

从性能的角度来看,我想这两种方法的开销大致相同,或者?

我应该选择哪种方法;同步实现async 方法或将同步方法调用显式包装在Task 中?

编辑

我正在处理的项目实际上是一个 .NET 4 项目,带有来自 Microsoft Async NuGet 包的 async / await 扩展。在 .NET 4 上,Task.Run 然后可以替换为 TaskEx.Run。在上面的示例中,我有意识地使用了 .NET 4.5 方法,希望使主要问题更加清晰。

【问题讨论】:

  • 对于这种情况,我相信Task.FromResult(SynchronousBarCreator()) 比Task.Run(...) 更好,因为它实际上并没有调度和运行任务来获得结果。
  • SynchronousBarCreator 是否长时间运行?是 CPU 受限,还是花时间等待其他事情?
  • 就我现在正在处理的情况而言,它不会长期运行。但它可能会出现在即将到来的场景中。
  • 这很令人困惑,也不是最佳实践。我的建议是:创建一个同时具有同步和异步方法的接口这种方法的一个很好的例子是 WebClient、EF 等。 . 或创建 2 个接口一个用于同步,另一个用于异步方法。

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


【解决方案1】:

当您必须从接口实现异步方法并且您的实现是同步的时,您可以使用 Ned 的解决方案:

public Task<Bar> CreateBarAsync()
{
    return Task.FromResult<Bar>(SynchronousBarCreator());
}

使用此解决方案,该方法看起来是异步的,但实际上是同步的。

或者你提出的解决方案:

  Task<Bar> CreateBarAsync()
  {
    return Task.Run(() => SynchronousBarCreator());
  }

这样方法是真正异步的。

您没有通用的解决方案来匹配“如何实现返回任务的接口方法”的所有情况。这取决于上下文:您的实现是否足够快,所以在另一个线程上调用它是没用的?何时调用此方法(它会冻结应用程序)如何使用此接口?甚至可以在另一个线程中调用您的实现吗?

【讨论】:

  • 感谢 Guillaume 的精彩总结。是否存在您会推荐我的第一个实现 (async Task&lt;Bar&gt; CreateBarAsync() { return SynchronousBarCreator(); }) 的上下文,或者这种方法从不可取吗?
  • 你不应该使用async over sync
  • Task.Run 几乎总是被过度使用。真正使用它的原因只有一个,那就是当您在 GUI 应用程序上进行计算时。但是很少有计算(相对于 CPU)密集型任务。
  • @Aron 我同意它经常被过度使用。当您必须调用同步的第三方库时,它也很有用,即使它执行了本可以异步完成的耗时任务。
【解决方案2】:

试试这个:

class Foo2 : IFoo
{
    public Task<Bar> CreateBarAsync()
    {
        return Task.FromResult<Bar>(SynchronousBarCreator());
    }
}

Task.FromResult 使用提供的值创建指定类型的已完成任务。

【讨论】:

  • 非常感谢,Ned,这对于我的场景来说似乎是一个非常好的解决方案。我也在寻找一种可以在带有 async 扩展的 .NET 4 上运行的解决方案,幸运的是,TaskEx 类还包含一个FromResult 方法。所以总而言之,我对这个解决方案非常满意。
  • 很高兴我能提供帮助
  • 大家好,你为什么评价这个答案?这个方法是同步运行的,但是有Async后缀,返回Task,这根本不正确。您应该返回 Task.Run(SynchronousBarCreator) 以使其异步。
  • +1 为了支持这个答案,我检查了微软为它的MemoryStream.ReadAsync 做了什么,这正是他们使用的。见referencesource.microsoft.com/#mscorlib/system/io/…;将它添加到您已经很好的答案中可能很有用:)
【解决方案3】:

如果您使用的是 .NET 4.0,则可以使用TaskCompletionSource&lt;T&gt;:

Task<Bar> CreateBarAsync()
{
    var tcs = new TaskCompletionSource<Bar>();
    tcs.SetResult(SynchronousBarCreator());
    return tcs.Task
}

最终,如果您的方法没有任何异步,您应该考虑公开一个同步端点 (CreateBar),它会创建一个新的 Bar。这样就没有惊喜,也不需要用多余的 Task 包装。

【讨论】:

  • 非常感谢,Yuval,也很高兴知道。这甚至可以在没有来自 Microsoft Async NuGet 包的 async 扩展的情况下工作,对吧?然而,我确实在我的 .NET 4 项目中包含了 async 扩展,然后TaskEx.FromResult 方法似乎是更有吸引力的方法。
  • 是的,您无需任何扩展即可使用它。 Task.FromResult 和 TaskEx.FromResult 只是 TaskCompletionSource 的包装器
  • 如果此方法将来是异步的,则可能会链接Task.Run(或等待Task.Delay,或使用ContinueWith) - 现在不异步执行它可能会导致不可预知的执行排序或导致竞争条件。
【解决方案4】:

为了补充其他答案,还有一个选项,我相信它也适用于 .NET 4.0:

class Foo2 : IFoo
{
    public Task<Bar> CreateBarAsync()
    {
        var task = new Task<Bar>(() => SynchronousBarCreator());
        task.RunSynchronously();
        return task;
    }
}

注意task.RunSynchronously()。与Task&lt;&gt;.FromResult 和TaskCompletionSource&lt;&gt;.SetResult 相比,它可能是最慢的选项,但有一个微妙但重要的区别:错误传播行为。

上述方法将模仿async 方法的行为,其中异常永远不会在同一个堆栈帧上抛出(展开),而是以休眠状态存储在Task 对象中。调用者实际上必须通过await task 或task.Result 观察它,此时它将被重新抛出。

Task&lt;&gt;.FromResult 和 TaskCompletionSource&lt;&gt;.SetResult 不是这种情况,SynchronousBarCreator 抛出的任何异常都将直接传播给调用者,展开调用堆栈。

我在这里有更详细的解释:

Any difference between "await Task.Run(); return;" and "return Task.Run()"?

附带说明,我建议在设计界面时添加取消条款(即使当前未使用/实现取消):

interface IFoo
{
    Task<Bar> CreateBarAsync(CancellationToken token);
}

class Foo2 : IFoo
{
    public Task<Bar> CreateBarAsync(CancellationToken token)
    {
        var task = new Task<Bar>(() => SynchronousBarCreator(), token);
        task.RunSynchronously();
        return task;
    }
}

【讨论】:

  • 非常高兴知道,Noseratio,非常感谢您提供这些额外信息!
  • 如果你在async方法中等待之前抛出异常,异常会在同一个栈上抛出还是存储在同一个栈上?
  • @Guillaume,是的,异常也会被存储,即使是从async方法的同步部分抛出的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-03-27
  • 2016-09-23
  • 1970-01-01
  • 1970-01-01
  • 2014-08-14
  • 1970-01-01
  • 2017-03-04
相关资源
最近更新 更多