Web API同步调用最佳实践

3

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

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() - Big Daddy
客户端或服务器上的死锁? - Stephen Cleary
尝试返回结果而不是等待。使用awaitreturn - Amit Kumar Ghosh
这不会导致死锁,因为你已经使用了CA(false)。 - usr
我们在这里很苦恼 :( - Amit Kumar Ghosh
显示剩余2条评论
3个回答

2
如果它是可以在后台运行并且不被强制同步的东西,请尝试将调用异步方法的代码(包装在Task.Run()中)。我不确定这是否会解决“死锁”问题(如果它是异步的,则是另一个问题),但如果您想从异步/等待中受益,如果您没有完全异步,除非在后台线程中运行,否则我不确定有什么好处。我有一个案例,在其中添加了Task.Run()(在我的情况下,从我更改为异步的MVC控制器)并调用异步方法,不仅略微提高了性能,而且在更重的负载下提高了可靠性(不确定是否是“死锁”,但似乎类似)。
您会发现,一些人认为使用Task.Run()是一种糟糕的方式,但在我的情况下,我确实找不到更好的方法来解决它,并且它似乎确实有所改进。也许这是那种存在理想方式与使其在您所处的不完美情况下工作的方式之间的差异。 :-)
[由于请求代码而更新]
因此,正如其他人发布的那样,您应该“始终完全异步”。在我的情况下,我的数据不是异步的,但我的UI是。因此,我尽可能地异步下降,然后以使 sense 的方式用Task.Run包装我的数据调用。我认为这就是诀窍,要确定是否可以并行运行事物,否则,您只是同步的(如果使用异步并立即解决它,强制等待答案)。我有很多可以并行执行的读取。
在上面的示例中,我认为您必须尽可能异步化,然后在某些时候确定可以启动线程并独立于其他代码执行操作的位置。假设您有一个保存数据的操作,但实际上不需要等待响应-您正在保存它并完成了。您唯一需要注意的是不要在等待该线程/任务完成之前关闭程序。在您的代码中,它适合您的地方由您决定。
语法非常简单。我采用了现有代码,将控制器更改为异步返回先前返回的我的类的任务。
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;

请参考微软官方文档获取更好的示例:https://msdn.microsoft.com/zh-cn/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.Run的代码示例吗?谢谢。 - Rick
我添加了更多信息以涵盖两种情况。很抱歉这段文字有点长,但为了我自己能够完全理解它们,我必须对它们进行详细的阐述。 - Gary Wolfe

1
使用Task<T>.Result相当于执行Wait,这将在线程上执行同步阻塞。在WebApi上拥有异步方法,然后让所有调用者同步地阻塞它们,实际上使WebApi方法同步。在负载下,如果同时等待的数量超过服务器/应用程序线程池,则会产生死锁。
因此,请记住“一路异步”的经验法则。您希望长时间运行的任务(获取List集合)是异步的。如果调用方法必须是同步的,则希望尽可能接近“底层”将其从异步转换为同步(使用Result或Wait)。将长时间运行的过程保持异步,并尽可能缩短同步部分的时间。这将大大减少线程被阻塞的时间。
例如,您可以执行以下操作。
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使您可以在更少的线程上执行更多操作。 在死锁发生之前,可以在更少的线程上进行更多并发的长时间运行的进程。

我正在尝试使用你的例子,但是在一个不是async的方法中如何使用await呢?谢谢。 - Rick
我收到编译器错误:"Await 操作符只能在 Async 方法中使用"。 - Rick
@Riccardo 如果你能创建一个异步返回List<T>的方法,我会这样做。然后从同步方法调用帮助方法。另一个选择是增加WebApi应用程序运行的应用程序池中可用的工作线程数。请注意,这不是一个万能药,线程创建有很大的开销。真正的答案是长时间运行的任务应该异步处理。我更新了我的答案以反映这些变化。 - Gerald Davis

0
你遇到的死锁问题一定是源于其他原因。你在这里使用了ConfigureAwait(false),可以防止死锁。解决这个bug,你就没问题了。
请参考我们是否应该默认切换到异步I/O?,答案是“不”。你应该根据情况决定,并在收益大于成本时选择异步。重要的是要理解异步IO会带来生产力成本。在非GUI场景下,只有少数有针对性的场景从异步IO中获得任何好处。然而,在这些情况下,好处可能是巨大的。
这里还有另一个有用的帖子:https://dev59.com/ml8f5IYBdhLWcg3wD_Av#25087273

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