如何实现返回Task<T>类型的接口方法?

42

我有一个接口

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

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

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

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

我应该如何实现使用同步方法创建BarFoo2类?

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

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

编译器会警告在方法签名中使用 async

这个异步方法缺少 'await' 操作符,将以同步方式运行。考虑使用 'await' 运算符等待非阻塞的 API 调用,或者使用 'await Task.Run(...)' 在后台线程上处理 CPU 绑定型工作。

或者,我可以显式地实现该方法返回 Task<Bar>。但在我看来,代码看起来就不太易读:

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

从性能角度来看,我认为这两种方法的开销大致相同,对吗?

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

编辑

我正在使用来自Microsoft Async NuGet包的async / await扩展的.NET 4项目。在.NET 4上,Task.Run可以替换为TaskEx.Run。我故意在上面的示例中使用了.NET 4.5方法,希望更清楚地表达主要问题。


2
对于这种情况,我认为Task.FromResult(SynchronousBarCreator())Task.Run(...)更好,因为它不会实际安排和运行任务来获取结果。 - Steve Guidi
SynchronousBarCreator 是一个长时间运行的程序吗?它是 CPU 密集型的,还是大部分时间都在等待其他操作完成? - CodesInChaos
在我现在处理的情况下,它不是长时间运行。但在即将到来的场景中可能会是。 - Anders Gustafsson
这非常令人困惑,也不是最佳实践。我的建议是:创建一个接口,其中包含同步和异步方法,这种方法的好例子有 WebClientEF 等等,或者创建两个接口,一个用于同步方法,另一个用于异步方法。 - Jaider
4个回答

28

当您需要从接口实现异步方法,而您的实现是同步的时,您可以使用Ned的解决方案:

public Task<Bar> CreateBarAsync()
{
    return Task.FromResult<Bar>(SynchronousBarCreator());
}
使用这个解决方案后,该方法看起来是异步的但实际上是同步的。

或者你提出的解决方案:

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

这种方式确实是异步的。

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


谢谢你的精彩总结,Guillaume。你是否认为有任何情况下会推荐我第一次实现(async Task<Bar> CreateBarAsync() { return SynchronousBarCreator(); }),或者这种方法从来不可取? - Anders Gustafsson
13
不应该使用异步(async)替代同步(sync)。 - Yuval Itzchakov
2
“Task.Run” 几乎总是被过度使用。实际上只有一个原因可以使用它,那就是当你在 GUI 应用程序中进行计算时。但相对于 CPU 而言,具有计算密集型任务的情况非常罕见。 - Aron
2
@Aron 我同意它经常被过度使用。即使它执行了可以异步完成的耗时任务,但当你必须调用一个同步的第三方库时,它也可能是有用的。 - Guillaume

24

试试这个:

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

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


1
非常感谢,Ned。这似乎是我场景下非常好的解决方案。我也在寻找一个能够在.NET 4上使用async扩展的解决方案,幸运的是,TaskEx类也包含了一个FromResult方法。总的来说,我对这个解决方案非常满意。 - Anders Gustafsson
3
很高兴我能够帮助。 - NeddySpaghetti
各位,你们为什么要评价这个答案?这个方法虽然带有 Async 后缀并返回 Task,但它实际上是同步运行的,这是完全不正确的。你应该返回 Task.Run(SynchronousBarCreator) 来使其异步化。 - Alexander Danilov
为了支持这个答案,我查看了微软在其MemoryStream.ReadAsync中所使用的内容,那正是他们所使用的。请参见https://referencesource.microsoft.com/#mscorlib/system/io/memorystream.cs,397;将其添加到您已经很好的答案中可能会很有用 :) - yair

8
如果你使用的是.NET 4.0,你可以使用 TaskCompletionSource<T>:
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方法似乎是更有吸引力的方法。 - Anders Gustafsson
3
是的,你可以不使用任何扩展来使用它。Task.FromResultTaskEx.FromResult只是TaskCompletionSource的一个包装器。 - Yuval Itzchakov
如果这个方法将来会是异步的,那么有理由链式调用Task.Run(或等待Task.Delay,或使用ContinueWith),现在不以异步方式执行可能会导致不可预测的执行顺序或引起竞争条件。 - Benjamin Gruenbaum

6
为了补充其他答案,还有一种选择,我相信这也适用于.NET 4.0:
class Foo2 : IFoo
{
    public Task<Bar> CreateBarAsync()
    {
        var task = new Task<Bar>(() => SynchronousBarCreator());
        task.RunSynchronously();
        return task;
    }
}

请注意task.RunSynchronously()。相比于Task<>.FromResultTaskCompletionSource<>.SetResult,它可能是最慢的选项,但有一个微妙但重要的区别:错误传播行为。
上述方法将模拟async方法的行为,其中异常永远不会在同一堆栈帧上抛出(展开它),而是存储在Task对象中处于休眠状态。调用者实际上必须通过await tasktask.Result观察它,此时它将被重新抛出。
这在Task<>.FromResultTaskCompletionSource<>.SetResult中并非如此,其中由SynchronousBarCreator引发的任何异常都将直接传播给调用者,展开调用堆栈。
我在这里有更详细的解释:

"await Task.Run(); return;" 和 "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;
    }
}

1
非常高兴知道这个,Noseratio,非常感谢你提供的额外信息! - Anders Gustafsson
如果在异步方法中等待之前抛出异常,那么异常会在同一堆栈上抛出还是也会被存储? - Guillaume
@Guillaume,是的,即使从“async”方法的同步部分抛出异常,该异常也将被存储。 - noseratio - open to work

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