【问题标题】:Web API Sync Calls Best PracticeWeb API 同步调用最佳实践
【发布时间】:2015-09-02 22:12:53
【问题描述】:

可能这个问题已经提出,但我从未找到明确的答案。假设我有一个 Web API 2.0 应用程序托管在 IIS 上。我想我理解最佳实践(防止客户端死锁)是始终使用异步方法,从 GUI 事件到 HttpClient 调用。这很好,而且有效。但是,如果我的客户端应用程序没有 GUI(例如窗口服务、控制台应用程序)但只有同步方法可以从中进行调用,那么最佳做法是什么?在这种情况下,我使用以下逻辑:

void MySyncMethodOnMyWindowServiceApp()
{
  list = GetDataAsync().Result().ToObject<List<MyClass>>();
} 

async Task<Jarray> GetDataAsync()
{
  list = await Client.GetAsync(<...>).ConfigureAwait(false);
  return await response.Content.ReadAsAsync<JArray>().ConfigureAwait(false);
}

但不幸的是,这仍然会在客户端上导致死锁,在随机机器上随机发生。

客户端应用程序在此停止并且永远不会返回:

list = await Client.GetAsync(<...>).ConfigureAwait(false);

【问题讨论】:

  • 看起来这可能是阻塞的 --> list = GetDataAsync().Result()
  • 客户端或服务器死锁?
  • 尝试返回结果而不是等待。 await 和返回
  • 这不会导致死锁,因为你使用了CA(false)。
  • 我们在这里挣扎:(

标签: c# iis .net-4.5 asp.net-web-api


【解决方案1】:

如果它可以在后台运行并且不强制同步,请尝试将代码(调用异步方法)包装在 Task.Run() 中。我不确定这会解决“死锁”问题(如果它不同步,那是另一个问题),但如果你想从 async/await 中受益,如果你一直没有异步,除非您在后台线程中运行它,否则我不确定是否有好处。我有一个案例,在几个地方添加 Task.Run()(在我的情况下,从我改为异步的 MVC 控制器)并调用异步方法不仅略微提高了性能,而且提高了可靠性(不确定它是一个“死锁”,但看起来类似)在较重的负载下。

你会发现使用 Task.Run() 被一些人认为是一种不好的方法,但在我的情况下我真的找不到更好的方法,而且它看起来确实是一个改进。也许这是其中一种理想的方法,而不是让它在你所处的不完美情况下工作的方法。:-)

[因请求代码而更新]

因此,正如其他人发布的那样,您应该“一直异步”。就我而言,我的数据不是异步的,但我的 UI 是异步的。所以,我尽可能地异步,然后我用 Task.Run 以合理的方式包装我的数据调用。我认为,这就是弄清楚事情是否可以并行运行是否有意义的诀窍,否则,你只是在同步(如果你使用 async 并立即解决它,迫使它等待答案)。我有许多可以并行执行的读取操作。

在上面的例子中,我认为你必须尽可能地异步,然后在某个时候确定你可以在哪里分离线程并独立于其他代码执行操作。假设您有一个保存数据的操作,但您实际上并不需要等待响应——您正在保存它并完成了。您可能需要注意的唯一一件事是不要在不等待该线程/任务完成的情况下关闭程序。在您的代码中哪里有意义取决于您。

语法很简单。我采用现有代码,将控制器更改为异步返回我的类的任务,该任务以前被返回。

var myTask = Task.Run(() =>
{
   //...some code that can run independently....  In my case, loading data
});
// ...other code that can run at the same time as the above....

await Task.WhenAll(myTask, otherTask); 
//..or...
await myTask;

//At this point, the result is available from the task
myDataValue = myTask.Result;

有关可能更好的示例,请参阅 MSDN: https://msdn.microsoft.com/en-us/library/hh195051(v=vs.110).aspx

[更新 2,与原始问题更相关]

假设您的数据读取是异步方法。

private async Task<MyClass> Read()

您可以调用它,保存任务,并在准备好时等待它:

var runTask = Read();
//... do other code that can run in parallel

await runTask;

因此,为此目的,调用异步代码,这是原始发布者所要求的,我认为你不需要 Task.Run(),尽管我认为你不能使用“等待”,除非你'重新使用异步方法 - 您需要等待的替代语法。

诀窍在于,如果没有一些代码可以并行运行,那么它就没有什么意义,所以考虑多线程仍然是重点。

【讨论】:

  • 你可以给我一个如何在我的代码上使用 Task.Run 的例子吗?谢谢
  • 我添加了更多信息以涵盖两种情况。抱歉,篇幅较长,我必须通过它们来完全理解。
【解决方案2】:

使用Task&lt;T&gt;.Result 等效于Wait,它将在线程上执行同步块。在 WebApi 上拥有异步方法,然后让所有调用者同步阻止它们,从而有效地使 WebApi 方法同步。在负载下,如果同时等待的数量超过服务器/应用程序线程池,您将死锁。

所以请记住经验法则“一直向下同步”。您希望长时间运行的任务(获取列表集合)是异步的。如果调用方法必须是同步的,您希望从异步到同步的转换(使用结果或等待)尽可能接近“地面”。保持它们长时间运行的进程异步,并使同步部分尽可能短。这将大大减少线程被阻塞的时间。

例如,你可以做这样的事情。

void MySyncMethodOnMyWindowServiceApp()
{
   List<MyClass> myClasses = GetMyClassCollectionAsync().Result;
}

Task<List<MyClass>> GetMyListCollectionAsync()
{
   var data = await GetDataAsync(); // <- long running call to remote WebApi?
   return data.ToObject<List<MyClass>>();
}

关键部分是长时间运行的任务保持异步并且没有被阻塞,因为使用了等待。

也不要将响应能力与可扩展性混淆。两者都是异步的正当理由。是的,响应能力是使用异步的一个原因(以避免在 UI 线程上阻塞)。您是对的,这不适用于后端服务,但这不是在 WebApi 上使用异步的原因。 WebApi 也是一个非 GUI 后端进程。如果异步代码的唯一优势是 UI 层的响应能力,那么 WebApi 将是从头到尾同步代码。使用异步的另一个原因是可伸缩性(避免死锁),这就是为什么 WebApi 调用是异步的。保持长时间运行的进程异步有助于 IIS 更有效地利用有限数量的线程。默认情况下,每个核心只有 12 个工作线程。这可以提高,但这也不是灵丹妙药,因为线程相对昂贵(每个线程大约 1MB 开销)。 await 让您事半功倍。在发生死锁之前,在更少的线程上更多的并发长时间运行的进程。

【讨论】:

  • 我正在尝试使用您的示例,但是如何在非异步方法中使用 await?谢谢
  • 我得到编译器错误:“等待运算符只能在异步方法中使用”
  • @Riccardo 如果您可以创建一个返回 List 的 aync 方法,我会这样做。然后从同步方法中调用辅助方法。另一种选择是增加运行 WebApi 应用程序的应用程序池上可用的工作线程数量。要理解这不是灵丹妙药,线程创建会产生大量开销。真正的答案是应该异步处理长时间运行的任务。我更新了答案以反映这些变化。
【解决方案3】:

您遇到的死锁问题一定源于其他原因。您对ConfigureAwait(false) 的使用可防止此处出现死锁。解决问题,你就没事了。

请参阅Should we switch to use async I/O by default?,其答案为“否”。您应该根据具体情况做出决定,并在收益超过成本时选择异步。重要的是要了解异步 IO 具有与之相关的生产力成本。在非 GUI 场景中,只有少数目标场景从异步 IO 中获得任何好处。不过,好处可能是巨大的,但仅限于这些情况。

这是另一个有用的帖子:https://stackoverflow.com/a/25087273/122718

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多