关于异步/等待与系统资源消耗和效率的问题

11
简短版:在循环中调用数千次异步方法,并且这些方法可能会调用其他异步方法时,异步调用的扩展性如何?线程池会爆满吗?
我一直在阅读和实验 TPL 和 Async,但在某些方面仍存在困惑,例如异步调用的扩展性。以下直奔主题。
异步调用 对于 IO,使用异步比新建线程/启动任务更好,但据我所知,在不使用不同线程的情况下执行异步操作是不可能的,这意味着异步必须在某个时刻使用其他线程/启动任务。 因此,我的问题是:相对于系统资源,代码 A 怎样比代码 B 更好?
代码 A
// an array with 5000 urls.
var urls = new string[5000];

// list of awaitable tasks.
var tasks = new List<Task<string>>(5000);

HttpClient httpClient;

foreach (string url in urls)
{
    tasks.Add(httpClient.GetStringAsync(url));
}

await Task.WhenAll(tasks);

Code B

...same variables as code A...

foreach (string url in urls)
{
    tasks.Add(
              Task.Factory.StartNew(() =>
              {
                // This method represents a
                // synchronous version of the GetStringAsync.
                httpClient.GetString(url);
              })
             );
}

await Task.WhenAll(tasks);

这让我想到了以下问题:
1 - 循环中应避免异步调用吗?
2 - 异步调用的最大合理数量是多少,或者任何数量的异步调用都可以吗?这如何扩展?
3 - 异步方法在底层每个调用是否启动任务?
我使用1000个URL进行测试,线程池工作线程的使用数量甚至没有达到30个,而IO完成线程的数量始终约为5个。 我的实际实验
我创建了一个带有简单异步控制器的网络应用程序。该页面由一个包含文本区域的表单组成,用户在其中输入希望请求/处理的所有URL。
提交后,使用HttpClient.GetUrlAsync方法在循环中请求URL,就像上面的代码A 一样。
有趣的是,如果我提交1000个URL,需要大约3分钟才能完成所有请求。
另一方面,如果我从3个不同的选项卡(即客户端)中的每个选项卡提交3个表单,每个表单中都有1000个URL,则结果需要更长时间(约10分钟),这真的让我感到困惑,因为根据MSDN的定义,它不应该花费比3分钟更长的时间,特别是即使在同时处理所有请求时,从线程池中使用的线程数也仅为25条,这意味着资源根本没有得到充分利用!
现在的工作方式使得这种类型的应用远非可扩展(假设我有大约5000个客户端一直请求一堆URL),而且我无法看到异步是如何发出多个IO请求的。
关于该应用程序的进一步说明
客户端:
1.用户进入网站
2.在文本区域中键入1000个URL
3.提交URL
服务器端:
1.接收URL数组
2.执行代码
foreach (string url in urls)
{
    tasks.Add(GetUrlAsync(url));
}

await Task.WhenAll(tasks);
//at this point the thread is
// returned to the pool to receive
// further requests.
  1. 通知客户端工作已完成

请让我来为您解答!谢谢。


有一件事需要知道 - 由于NodeJS中的异步功能来自IO,因此其可扩展性完全取决于您处理的IO /资源类型。例如,用于数据库查询和HTTP查询的异步方式具有非常不同的可扩展性。 - vitaly-t
1个回答

7
据我了解,不使用不同的线程执行异步操作是不可能的,这意味着异步必须在某个时候使用其他线程/启动任务。 不对。正如我在我的博客上所描述的那样,纯异步方法不会阻塞线程。
因此,我的问题是:关于系统资源,代码 A 怎么比代码 B 更好?
A 使用的线程比 B 少。
(附带一提,请不要使用 StartNew。它已经过时了,并且具有非常危险的默认参数值。请改用 Task.Run。如果您从博客文章中得到这个想法/代码,请将这个消息传递下去。StartNew 是一种似乎正在接管互联网的癌症。)
异步调用在循环中应该避免吗?
不,没问题。
一个合理的最大异步调用次数是否存在,或者任何数量的异步调用都可以被触发?
任意数量的调用都可以,只要您的后端资源能够处理它。

这个系统的扩展性如何?

.NET上的异步I/O几乎总是在底层使用IOCP(I/O完成端口),通常被认为是Windows上可用的最具可扩展性的I/O形式。

在幕后,异步方法是否为每个调用启动一个任务?

是和否。每个异步方法的执行都由一个Task实例表示,但这些不代表正在运行的任务-它们不代表线程。

我称异步任务Promise Tasks,而不是Delegate Tasks(实际在线程池上运行的任务)。

真的让我困惑了

当您测试URL请求时要注意的一件事是,.NET内置了对URL请求的自动限制。尝试将ServicePointManager.DefaultConnectionLimit设置为int.MaxValue


嗨,史蒂芬,谢谢你的回复,提供了非常好的信息!当我说异步操作不可能没有额外线程时,我的表述不是很清楚。我指的是在整个过程中不需要,但在某些阶段必须使用其他线程来处理请求,否则操作将会被阻塞。在请求到达驱动程序之前,必须由除主线程以外的线程来处理此请求,这点我对IRP很熟悉,并且知道当IRP准备就绪时会唤醒一个线程。但是,在请求到达驱动程序之前,必须使用其他线程来处理此请求,否则操作将会被阻塞,这意味着在某个时候会使用新线程,也许是编译器创建的状态机,我不确定。对吧? - victor
请求是否同步传递给驱动程序? - victor
ServicePointManager.DefaultConnectionLimit 默认情况下为 2147483647。 - victor
@victor:请求是同步传递的。在最新版本的.NET(4.6)中,默认情况下DefaultConnectionLimit仅为int.MaxValue;以前它是2 - Stephen Cleary
看起来有其他东西在限制我的请求。我在控制台应用程序上测试了完全相同的代码,它在<50秒内运行,而在使用IIS时需要约115秒。我一整天都在尝试解决这个问题,但仍然没有任何进展。有时所有的994个请求都可以在不到一秒的时间内发出,但需要100多秒才能完成,这很奇怪,因为我将超时设置为10。我感觉好像有些东西正在限制我可以用来处理APC的线程数。 - victor
@victor Windows客户端版本有10个并发请求的限制。尽管如此,您仍然有系统http队列大小、最大并发请求数和ServicePointManager.DefaultConnectionLimit。 - Walter Macambira

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