Web API 同步调用最佳做法



可能这个问题已经提出来了,但我从未找到明确的答案。假设我有一个托管在 IIS 上的 Web API 2.0 应用程序。我想我理解最佳实践(防止客户端死锁)始终使用从 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);

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

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

[由于代码请求而更新]

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

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

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

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(),尽管我认为你不能使用"await",除非你是异步方法 - 你需要一个替代语法来等待。

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

使用 Task<T>.Result 等效于Wait,它将在线程上执行同步块。 在 WebApi 上使用异步方法,然后让所有调用方同步有效地阻止它们,使 WebApi 方法同步。 在负载下,如果并发等待数超过服务器/应用程序线程池,您将死锁。

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

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

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>>();
}

关键部分是长时间运行的任务保持异步且未被阻止,因为使用了 await。

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

您遇到的死锁问题必须源于其他原因。您使用ConfigureAwait(false)可以防止此处的死锁。解决错误,你没事。

请参阅我们是否应该切换到默认使用异步 I/O?,答案是否定的。您应该根据具体情况做出决定,并在收益大于成本时选择异步。请务必了解异步 IO 具有与之相关的生产力成本。在非 GUI 方案中,只有少数目标方案从异步 IO 中获得任何好处。不过,好处可能是巨大的,但仅限于这些情况。

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

最新更新