【问题标题】:Starting tasks returned by iterator methods in parallel并行启动迭代器方法返回的任务
【发布时间】:2026-01-11 23:00:02
【问题描述】:

先看看这个简单的方法:

Public Iterator Function GetLongRunningTasks(count As Long) As IEnumerable(Of Task)
    For i = 1 To count
        Yield Task.Delay(3000)
    Next
End Function

此方法返回指定数量的任务,每个任务在启动后 3 秒完成。我们称其为在极差的网络连接上模拟网络 API 调用(没关系)。

我的问题是一个简单的迭代将一次启动一个任务,因此每次迭代之间会发生 3000 毫秒的延迟。

    For Each t In GetLongRunningTasks(50)
        Await t
    Next ' this takes ~150 seconds to complete (50x3000ms)

我想做的是一次启动所有 50 个任务,然后进入 foreach 循环。最好坚持上面的例子,这样做的正确方法是什么?

编辑

正如 Stephen 所建议的,一种解决方案是遍历 GetLongRunningTasks(50).ToList()。也许只有我一个人,但我认为在阅读代码时使用 ToList 的原因并不明显。

不知道下面的sn-p是不是一模一样?

    Dim tasks As New List(Of Task)
    tasks.AddRange(GetLongRunningTasks(50))

    For Each t In tasks
        Await t
    Next

【问题讨论】:

    标签: .net vb.net iterator task-parallel-library


    【解决方案1】:

    您可以调用ToList 来创建所有Tasks。然后你可以使用For Each(或者Task.WhenAll,如果你只是Awaiting每一个)。

    【讨论】:

      【解决方案2】:

      只是为斯蒂芬的回答添加一些解释:GetLongRunningTasks() 返回一个惰性迭代器,它仅在您迭代它时创建 Tasks。在您的原始代码中,每次迭代都会创建一个 Task,然后等待它完成,然后才开始另一个迭代,这又开始另一个 Task

      所以,您首先需要遍历整个集合以启动所有 Tasks 并等待它们完成,直到您拥有它们。 Stephen 的 ToList() 建议正是这样做的,而您的 AddRange() 也会这样做。

      如果您仍然不清楚,也许另一种方法会有所帮助:

      Dim tasks As New List(Of Task)
      For Each t in GetLongRunningTasks(50)
          tasks.Add(t)
      Next
      
      For Each t In tasks
          Await t
      Next
      

      另外,启动大量 IO-bound Tasks 很可能不是最有效的选择,以有限的并行度运行它们是。为此,您可以使用SemaphoreSlimWaitAsnyc(),或使用ActionBlock 和从TPL 数据流设置的MaxDegreeOfParallelism

      【讨论】: