如何在Swift并发中限制并发性(例如`maxConcurrentOperationCount`)?

19

我正在尝试执行一系列网络请求,并希望在新的 Swift 并发系统中限制并行任务的数量。在操作队列中,我们会使用maxConcurrentOperationCount。在 Combine 中,使用flatMap(maxPublishers:_:)。在新的 Swift 并发系统中有什么等效的方法?

例如,这可能不是特别相关,但考虑一下:

func downloadAll() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        for index in 0..<20 {
            group.addTask { try await self.download(index) }
        }

        try await group.waitForAll()
    }
}

这导致所有请求同时运行。

enter image description here

事实上,URLSession不遵守httpMaximumConnectionsPerHost的规定确实很有趣,但这并不是关键问题。我更关心的是如何在一系列并行运行的异步任务中限制并发程度。
1个回答

28
在达到一定计数之后,可以在循环中插入一个group.next()调用,例如:
func downloadAll() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        for index in 0..<20 {
            if index >= 6 { try await group.next() }
            group.addTask { try await self.download(index) }
        }

        try await group.waitForAll()
    }
}

每次最多只能有六个结果。

enter image description here


为了完整起见,我应该注意到在WWDC 2023中,苹果提出了一种替代模式:超越结构化并发的基础知识
withTaskGroup(of: Something.self) { group in
    for _ in 0 ..< maxConcurrentTasks {
        group.addTask {  }
    }
    while let <partial result> = await group.next() {
        if !shouldStop {
            group.addTask {  }
        }
    }
}

在这个例子中,可能会翻译成:

func downloadAll() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        for index in 0..<6 {
            group.addTask { try await self.download(index) }
        }
        var index = 6
        while try await group.next() != nil {
            if index < 20 {
                group.addTask { [index] in try await self.download(index) }
            }
            index += 1
        }
    }
}

投资品中的收益:

enter image description here

这个想法非常相似,即你可以在最大并发度上使用 group.addTask {…},但之后再使用 group.next() 来添加每个后续任务。这是另一种解决方法。


1
我该如何从Xcode中打开网络时间轴(如图所示)?(此外,非常干净的解决方案,感谢您的发布!) - emehex
@emehex - 这是来自 Charles Proxy 的屏幕截图,它是一款非常优秀的网络活动监视工具。 (对于诊断 API 流量也非常有帮助,但初始配置可能第一次设置时会有点棘手。)不幸的是,这是一款收费工具。您也可以使用 Instruments 的“兴趣点”工具,它是 Xcode 的一部分,如此处所述,但您需要在网络请求之前和之后添加 os_signpost 调用。 - Rob
此外,在 Xcode 的 Instruments 中,你可以使用“网络”模板,其中包括“HTTP 流量”工具,它会显示一个条形图,显示了在时间轴上运行的并发网络请求的数量,这传达了相同基本的网络活动信息,但布局略有不同。例如,这里是包括“HTTP 流量”和“兴趣点”的 Instruments 输出(显然已经添加了 os_signpost 调用来标记每个网络请求的 .begin.end)。https://i.stack.imgur.com/xR4Ii.png - Rob
1
这可能更适合作为一个单独的问题,但是如果第一组任务已经在进行中,您将如何添加更多任务?比如在这20个操作的中间,另一个函数想要添加10个任务,或者类似的情况怎么办? - RL2000
2
@RL2000 - 你可以使用 Swift Async Algorithms 中的 AsyncChannel。例如,请参见 https://dev59.com/5cTsa4cB1Zd3GeqPINX7#73072799 或 https://dev59.com/I8Pra4cB1Zd3GeqPt_11#75730483 的示例。 - Rob

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