运行多个异步任务 - 链接与并行的结果更快

本文关键字:并行 结果 链接 异步 任务 运行 | 更新日期: 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();
}

调用代码(并发版本和串行版本(保持不变。