运行多个异步任务 - 链接与并行的结果更快
本文关键字:并行 结果 链接 异步 任务 运行 | 更新日期: 2023-09-27 18:34:48
我想同时进行 7 个不同的 API 调用,并希望在每次调用完成后更新 UI。
我一直在摆弄两种不同的方法来做到这一点,链接请求,并同时(并行(关闭所有请求。
两者都似乎有效,但由于某种原因,我的并行任务比我链接它们时花费的时间要长得多。
我是TPL/并行性的新手,所以可能是我的代码不正确,但是链接请求不会花费更长的时间,因为每个请求都必须在下一个开始之前完成?而不是并行,他们一次出去,所以你只需要等待最慢的?
如果您在我的逻辑或代码中看到错误,请告诉我。我对我得到的响应时间感到满意,但我不明白为什么。
我的"链接"代码:
await (Task.Run(() => WindLookup_DoWork()).
ContinueWith((t) => WindLookup_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()).
ContinueWith((t) => OFAC_DoWork()).ContinueWith((t) => OFAC_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()).
ContinueWith((t) => BCEGS_DoWork()).ContinueWith((t) => BCEGS_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()).
ContinueWith((t) => BOPTerritory_DoWork()).ContinueWith((t) => BOPTerritory_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()).
ContinueWith((t) => TerrorismTerritory_DoWork()).ContinueWith((t) => TerrorismTerritory_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()).
ContinueWith((t) => ProtectionClass_DoWork()).ContinueWith((t) => ProtectionClass_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()).
ContinueWith((t) => AddressValidation_DoWork()).ContinueWith((t) => AddressValidation_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
我的"并行"代码:
List<Task> taskList = new List<Task>();
taskList.Add(Task.Run(() => WindLookup_DoWork()).
ContinueWith((t) => WindLookup_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
taskList.Add(Task.Run(() => BCEGS_DoWork()).
ContinueWith((t) => BCEGS_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
taskList.Add(Task.Run(() => BOPTerritory_DoWork()).
ContinueWith((t) => BOPTerritory_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
taskList.Add(Task.Run(() => TerrorismTerritory_DoWork()).
ContinueWith((t) => TerrorismTerritory_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
taskList.Add(Task.Run(() => ProtectionClass_DoWork()).
ContinueWith((t) => ProtectionClass_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
taskList.Add(Task.Run(() => OFAC_DoWork()).
ContinueWith((t) => OFAC_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
taskList.Add(Task.Run(() => AddressValidation_DoWork()).
ContinueWith((t) => AddressValidation_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
await Task.WhenAll(taskList.ToArray());
我基本上转换了旧的后台工作线程代码,这就是为什么有DoWork方法和更新UI的"回调"方法。
DoWork 方法向 API 调用 POST 方法,流程结果只是使用响应 xml 填充文本区域。
我一直在摆弄两种不同的方法来做到这一点,链接请求,并同时(并行(关闭所有请求。
区分并发性和并行性非常重要。在您的情况下,您只想同时执行所有这些操作,这是一种并发形式。并行性是一种更具体的技术,它使用多个线程来实现并发性,适用于 CPU 密集型工作。但是,在您的情况下,工作是 I/O 密集型的(API 请求(,在这种情况下,更合适的并发形式是异步。异步是一种不使用线程的并发形式。
两者都似乎有效,但由于某种原因,我的并行任务比我链接它们时花费的时间要长得多。
我不确定为什么它们会更长,但一个常见问题是同时请求的数量受到限制,无论是在客户端 ( ServicePointManager.DefaultConnectionLimit
(,还是在服务器端(例如,请参阅并发请求和会话状态(。
如果您在我的逻辑或代码中看到错误,请告诉我。
不幸的是,TPL 很难从参考文档或 IntelliSense 中学习,因为有很多方法和类型只应在非常特定的情况下使用。特别是,不要在方案中使用 ContinueWith
;它StartNew
有同样的问题(两个链接都指向我的博客(。
更好(更可靠且更易于维护(的方法是引入一些帮助程序方法:
async Task WindLookupAsync()
{
await Task.Run(() => WindLookup_DoWork());
WindLookup_ProcessResults();
}
// etc. for the others
// Calling code (concurrent):
await Task.WhenAll(
WindLookupAsync(),
BCEGSAsync(),
BOPTerritoryAsync(),
TerrorismTerritoryAsync(),
ProtectionClassAsync(),
OFACAsync(),
AddressValidationAsync()
);
// Calling code (serial):
await WindLookupAsync();
await BCEGSAsync();
await BOPTerritoryAsync();
await TerrorismTerritoryAsync();
await ProtectionClassAsync();
await OFACAsync();
await AddressValidationAsync();
使用重构的代码,不需要ContinueWith
或显式TaskScheduler
。
但是,每个请求仍在为每个请求刻录一个线程池线程。如果这是一个桌面应用程序,它不是世界末日,但它没有使用最佳解决方案。正如我在本答案开头提到的,更适合这个问题的是异步而不是并行。
要使代码异步,您应该首先从 POST API 调用开始,然后将其更改为使用异步版本并使用 await
调用它。(旁注:WebClient
确实有异步方法,但请考虑更改为 HttpClient
,这更自然地适合async
(。一旦您使用await
调用该 POST API,这将要求您_DoWork
方法变为异步(并返回 Task
而不是 void
(。此时,您可以更改上面的帮助程序方法以直接await
这些方法,而不是使用 Task.Run
,例如:
async Task WindLookupAsync()
{
await WindLookup_DoWork();
WindLookup_ProcessResults();
}
调用代码(并发版本和串行版本(保持不变。