将同步 API 包装到 Async 方法中

Posted

技术标签:

【中文标题】将同步 API 包装到 Async 方法中【英文标题】:Wraping sync API into Async method 【发布时间】:2014-02-01 08:24:02 【问题描述】:

我的 MVC 应用程序使用一个库,并且该库的一些方法在内部调用 WCF 服务。从此 DLL 公开的所有方法都是同步的(它们都不返回 Task 或 Task),并且由于我们不拥有该程序集,因此无法将它们转换为 Async API。

但是,由于这些方法调用 WCF 服务,它们是网络绑定的(因此理想情况下它们应该是异步的)。

我想在我的 MVC 应用程序中使用异步控制器操作以使其更具可扩展性。我的问题是当一种方法本质上是同步的时,如何使整个方法管道可以等待。

异步操作 --> 等待异步方法 --> 等待异步方法 2 --> 从库中同步方法?

我应该使用 TaskCompletionSource 还是 Task.FromResult 来包装库方法调用?

另外,如果我使用上述方法,我的代码会比同步版本更具可扩展性吗?

【问题讨论】:

在异步中包装同步方法对可扩展性没有任何帮助。它只是将执行移到另一个线程上。如果该线程有重要的事情要做(如 UI 线程),这可能会提高响应能力。参见 Steven Toub 的帖子:blogs.msdn.com/b/pfxteam/archive/2012/03/24/10287244.aspx 【参考方案1】:

我的问题是当一种方法本质上是同步时,如何使整个方法管道可以等待。

你不能。唯一的解决办法就是重写dll。

我应该使用 TaskCompletionSource 还是 Task.FromResult 来包装库方法调用?

都没有。

另外,如果我使用上述方法,我的代码会比同步版本更具可扩展性吗?

没有。它将稍微较少可扩展。

【讨论】:

谢谢斯蒂芬。但是,我正在阅读您的一篇博客,您提到“有一种方法可以将您的方法堆栈部分转换为异步”,我认为我处于这种情况。知道同步 API 进行网络绑定调用,我无法抗拒自己以异步方式调用它,但由于它是服务器应用程序,Task.Run 是不行的。对于这种情况,.NET 似乎应该有一个更好的解决方案,因为堆栈中的一个方法是同步的,而我无法控制它,这使我无法从异步中获益。 :( 补充一下,根据我的知识(我通过阅读你的大部分博客了解到:)),不管在这个同步方法调用之前我有多少等待可用的方法,它是用包装的TCS ,最终请求线程将最终执行同步方法并被阻塞。这减少了从控制器操作开始创建整个调用堆栈等待的目的。 It seems like .NET should have a better solution for this kind of scenario... 问题是:它将如何工作? 那么,将方法堆栈部分转换为异步的最佳选择应该是什么?另外,如果我可以使用 Task.WhenAll(task1, task2) 在异步 API 中包装同步方法(使用 Task.FromResult 或类似方法),那么它不会提供一些性能提升吗? 在 ASP.NET 中,最好的选择是直接调用同步方法。使用Task.FromResult 毫无意义 - 你只会造成更多的开销。您可以将同步方法包装在Task.Run 中以同时执行多项操作(使单个请求更快);但是,由于每个请求都使用 多个 线程,因此这很容易降低整个服务器的可扩展性。【参考方案2】:

TaskCompletionSource<T>是一种创建傀儡任务的方法,它可以在你喜欢的任何时候完成,并且可以在你喜欢的任何时候让它出错。这意味着,这在您的情况下是理想的,因为您无法控制您尝试使用的 API 方法。以下示例将为您提供一个良好的开端。

public class HomeController : Controller
 
    public async Task<ActionResult> Index()
    
    ViewBag.Message = await ProcessRequest();

    return View();
    


    //TResult -> can be of any built-in or custom type that you should decide.
    Task<TResult> ProcessRequest()
    

    // Make a TaskCompletionSource so we can return a puppet Task
    TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();

    // Call your sync API method
    SyncAPI syncApi = new SyncAPI();

    // Call api method and set the result or exception based on the output from the API //method.
    tcs.SetResult(TResult);

    // Return the puppet Task, which isn't completed yet
    return tcs.Task;
    

【讨论】:

谢谢奈尔。这就是我目前实施的。但是,如果您注意到异步调用是如何执行的,您将意识到最终包装同步 API 调用的 puppet 方法将在请求线程上执行,并且不会释放它以接受更多用户请求。因此,无论您在其上方有多少可等待的方法,您的请求线程最终都会在同步 API 调用时被阻塞。如果我错了,请纠正我。您可能想要 Response.Write 当前线程管理的 Id 在您的代码中作为概念证明。

以上是关于将同步 API 包装到 Async 方法中的主要内容,如果未能解决你的问题,请参考以下文章

使用async和await将同步方法包装成异步方法

将同步代码包装成异步方法的最佳方法是啥

为 C 消费包装 C++ 类 API

包装或不包装:在服务外观中包装数据访问

如何访问由 C# 中包装的非.net API 返回的数组?

如何在 C# 中包装 HttpClient 以实现可测试性