🤹

【Swift】TaskGroupで並列に処理するタスクの数を制限したい

2024/03/22に公開

はじめに

TaskGroupを使って、複数のタスクを並列に処理することができます。
以下の基本的な例では、100個のタスクを並列に処理しています。

TaskGroupを使った基本的な実装
Task {
    let numbers = try await echoNumbers(Array(0..<100))
    print(numbers) // 結果: [0, 1, ... 98, 99]
}

func echoNumbers(_ numbers: [Int]) async throws -> [Int] {
    try await withThrowingTaskGroup(of: Int.self) { group in
        for num in numbers {
            group.addTask { try await echoNumber(num) }
        }
    
        var result: [Int] = []
        for try await num in group {
            print(num)
            result.append(num)
        }
        
        return result.sorted()
    }
}

func echoNumber(_ number: Int) async throws -> Int { 
    try await Task.sleep(for: .milliseconds(Int.random(in: 500...1000)))
    return number
}

本記事では、TaskGroupで並列に処理するタスクの数を制限する方法を解説します。

例えば、並列に処理するタスクを20個に制限したいとします。
呼び出し元で値を20個ずつ渡して5回分のechoNumbersを実行するのは、効率が良くありません。

ひとつタスクが終わったらすぐに新たなタスクが追加され、常に20個のタスクが処理されていることが理想です。

実装

以下のように実装することで、並列に処理するタスクの数を制限することができます。
まず、limitTaskCountの分だけタスクを追加します。
そしてfor awaitで値を取り出すたびに、タスクが無くなるまでTaskGroupにタスクを追加していきます。

func echoNumbers(_ numbers: [Int]) async throws -> [Int] {
    try await withThrowingTaskGroup(of: Int.self) { group in
        let limitTaskCount = min(20, numbers.count)
        for index in 0..<limitTaskCount {
            group.addTask { try await echoNumber(numbers[index]) }
        }
    
        var result: [Int] = []
        var nextIndex = limitTaskCount
        for try await num in group {
            if nextIndex < numbers.count {
                let index = nextIndex
                group.addTask { try await echoNumber(numbers[index]) }
                nextIndex += 1
            }
            print(num)
            result.append(num)
        }

        return result.sorted()
    }
}

通常のfor文では、反復処理中にイテレーション対象を変更することはできません。
しかし、TaskGroupはfor await構文を使っていることから分かる通りAsyncSequenceです。
したがって、ループでTaskGroupから値を取り出しながらそのスコープ内でさらにタスクを追加するというコードを書くことができます。

参照

株式会社Never

Discussion