Joh*_*lly 7 linq task threadpool async-await
考虑:您有一组用户 ID,并希望从 API 加载由其 ID 表示的每个用户的详细信息。您希望将所有这些用户打包到某种集合中并将其发送回调用代码。并且您想使用 LINQ。
像这样的东西:
var userTasks = userIds.Select(userId => GetUserDetailsAsync(userId));
var users = await Task.WhenAll(tasks); // users is User[]
Run Code Online (Sandbox Code Playgroud)
当我的用户相对较少时,这对我的应用程序来说很好。但是,它出现了无法扩展的情况。当它到达成千上万的用户时,这导致同时触发了数千个 HTTP 请求,并且开始发生不好的事情。我们不仅意识到我们正在对我们正在使用的 API 发起拒绝服务攻击,而且我们还通过线程饥饿使我们自己的应用程序崩溃。
不是骄傲的一天。
一旦我们意识到我们的困境的原因是Task.WhenAll/Select组合,我们就能够摆脱这种模式。但我的问题是:
这里出了什么问题?
当我阅读有关该主题的内容时,Mark Heath 的异步反模式列表中的#6 似乎很好地描述了这种情况:“过度并行化”:
现在,这确实“有效”,但是如果有 10,000 个订单呢?我们已经用数千个任务淹没了线程池,可能会阻止其他有用的工作完成。如果 ProcessOrderAsync 对另一个服务(如数据库或微服务)进行下游调用,我们可能会因调用量过高而使该服务过载。
这真的是原因吗?我问的越多,我对async/ 的理解await就越不清晰。从许多文章中可以清楚地看出“线程不是任务”。这很酷,但我的代码似乎耗尽了 ASP.NET Core 可以处理的线程数。
那么是这样吗?我的Task.WhenAll和Select组合是否耗尽了线程池或类似的东西?或者还有其他我不知道的解释吗?
更新:
我把这个问题变成了一篇博客文章,里面有更多细节/华夫饼。你可以在这里找到它:https : //blog.johnnyreilly.com/2020/06/taskwhenall-select-is-footgun.html
您在这里已经有了一个很好的答案,但只是插话一下:
创建数千个任务没有问题。它们不是线程。
核心问题是你对 API 的使用太多了。因此,最好的解决方案将改变您调用该 API 的方式:
batch如果该 API 支持的话,请使用该 API 的路由。异步节流的标准模式是使用SemaphoreSlim,如下所示:
使用 varthrottler = new SemaphoreSlim(10);
var userTasks = userIds.Select(异步 userId =>
{
等待throttler.WaitAsync();
尝试 { 等待 GetUserDetailsAsync(userId); }
最后{throttler.Release(); }
});
var users = wait Task.WhenAll(tasks); // 用户是 User[]
同样,只有当您无法进行设计更改以避免首先避免数千次 API 调用时,这种限制才是最好的。
将线程、任务、异步、并行性放在一边,您所描述的是一个 N+1 问题,对于发生在您身上的事情,这是要避免的。当 N(您的用户数)很小时,一切都很好,但随着用户的增长,它会停止。
您可能想找到不同的解决方案。必须对所有用户都做这个操作吗?如果是这样,那么可能会切换到后台进程并为每个用户扇出。
任务是一种承诺,类似于 JavaScript。在 .NET 中,它们可能在一个单独的线程上完成——通常是线程池中的一个线程。
在 .NET Core 中,对于几乎肯定会出现的 HTTP 请求,如果未完成和等待点,它们通常会在单独的线程上完成。
您可能已经耗尽了线程池,但是由于您正在发出 HTTP 请求,我怀疑您已经耗尽了并发出站 HTTP 请求的数量。“ASP.NET 托管应用程序的默认连接限制为 10 个,所有其他应用程序的默认连接限制为 2 个。” 请参阅此处的文档。
有没有办法实现一些并行性而不消耗资源(线程或 http 连接)?- 是的。
这是我经常出于这个原因实现的模式,使用Batch()from morelinq。
IEnumerable<User> users = Enumerable.Empty<User>();
IEnumerable<IEnumerable<string>> batches = userIds.Batch(10);
foreach (IEnumerable<string> batch in batches)
{
Task<User> batchTasks = batch.Select(userId => GetUserDetailsAsync(userId));
User[] batchUsers = await Task.WhenAll(batchTasks);
users = users.Concat(batchUsers);
}
Run Code Online (Sandbox Code Playgroud)
您仍然会收到 10 个对 的异步 HTTP 请求GetUserDetailsAsync(),并且不会耗尽线程或并发 HTTP 请求(或至少最多使用 10 个)。
现在,如果这是一个频繁使用的操作或服务器GetUserDetailsAsync()在应用程序的其他地方大量使用,当您的系统负载不足时,您可能会遇到相同的限制,因此这种批处理并不总是一个好主意。天啊。
| 归档时间: |
|
| 查看次数: |
581 次 |
| 最近记录: |