본문 바로가기
Swift

Task Group

by SeoB-P 2023. 1. 14.

문제점

Swift Concurrency를 사용하다가 보면 이런 문제를 마주할 수 있다.

API가 많아졌을 때, 이를 한번에 호출하기가 쉽지 않다.

예를 들어 Home View를 구성하는데 세개의 View Component가 있고, 이 Component안의 데이터를 채우기 위해서는 세개의 API를 call해야한다고 가정을 해보자.

 

이 때 무지성으로 task안에 await를 하나씩 하나씩 넣으면 매우 비효율적인 모습이 되게된다.

Why?

Task안의 코드들은 마치 동기 코드인것 마냥 진행이된다.

따라서, getAData가 끝나지 않는다면 getCData는 실행되지 않는다.

A, B, C의 데이터는 각각이 독립적이어서 어떤 것이 먼저 실행되든 상관이 없음에도 불구하고,

getAData()가 끝나지 않으면 그 밑의 코드들은 실행이 되지 않는다.

 

이를 해결하기 위한 방법으로 async let을 이용하는 방법도 있다.

하지만 이또한 결국 사용하기 위해서는 await를 써야하기 때문에 밑과 같이 조금 어색한 코드가 되어버린다.

 

이를 해결하기 위해서 Task Group을 사용해본다.

Task Group

Task Group 을 사용하면 여러 Task들을 순서 상관없이 작업이 끝나는대로 Return을 할 수 있다.

 

실제로 순서상관없이 동시에 실행이 되는지 한번 예제를 통해 살펴보자. 예제 출처

struct SlowDivideOperation {

    let name: String
    let a: Double
    let b: Double
    let sleepDuration: UInt64

    func execute() async -> Double {
        do {
            // Sleep for x seconds
            try await Task.sleep(nanoseconds: sleepDuration * 1_000_000_000)
            let value = a / b
            return value
        } catch {
            return 0.0
        }
    }
}


let sampleA: SlowDivideOperation = .init(name: "sampleA", a: 10, b: 2, sleepDuration: 2)
let sampleB: SlowDivideOperation = .init(name: "sampleB", a: 10, b: 2, sleepDuration: 2)

Task {
    let start = Date.timeIntervalSinceReferenceDate
    await sampleA.execute()
    await sampleB.execute()
    let end = Date.timeIntervalSinceReferenceDate
    print(String(format: "Duration: %.2fs", end-start)) // Duration: 4.25s
}

위 코드를 실행시켜보면 execute() 한번당 2초의 Sleep을 주었기 때문에, 총 4초 정도가 걸림을 확인할 수 있는데 이는 앞서 말했던 문제점과 정확히 일치한다. 

 

그럼 이제 TaskGroup을 만들어서 위 코드들이 동시에 실행되게 만들어보자.

예제 코드

let operations = [
    SlowDivideOperation(name: "operation-0", a: 5, b: 1, sleepDuration: 5),
    SlowDivideOperation(name: "operation-1", a: 14, b: 7, sleepDuration: 1),
    SlowDivideOperation(name: "operation-2", a: 8, b: 2, sleepDuration: 3),
]

Task {
    print("Start Task!")
    let start = Date.timeIntervalSinceReferenceDate

    let allResults = await withTaskGroup(of: (String, Double).self, // child task return type
                                         returning: [String: Double].self, // group task return type
                                         body: { taskGroup in 
        // Loop through operations array
        for operation in operations {
            // Add child task to task group
            taskGroup.addTask {
                // Execute slow operation
                let value = await operation.execute()
                print("\(operation) is Done!")
                let middle = Date.timeIntervalSinceReferenceDate
                print(String(format: "Duration: %.2fs", middle-start))
                // Return child task result
                return (operation.name, value)
            }
        }
        // Collect results of all child task in a dictionary
        var childTaskResults = [String: Double]()
        for await result in taskGroup {
            // Set operation name as key and operation result as value
            childTaskResults[result.0] = result.1
        }
        // All child tasks finish running, thus task group result
        return childTaskResults
    })
    let end = Date.timeIntervalSinceReferenceDate
    print("End Task!")
    print(String(format: "Duration: %.2fs", end-start))  // Duration: 5.36s
}

하나하나씩 코드를 뜯어보자.

 

Operation의 배열을 만들었다.

만약 이 operation들이 동기적으로 실행된다면 약 9초 정도가 걸릴 것이다.

let operations = [
    SlowDivideOperation(name: "operation-0", a: 5, b: 1, sleepDuration: 5),
    SlowDivideOperation(name: "operation-1", a: 14, b: 7, sleepDuration: 1),
    SlowDivideOperation(name: "operation-2", a: 8, b: 2, sleepDuration: 3),
]

 

 

withTaskGroup(of:returning: body) 함수를 이용해서 TaskGroup을 만든다.

만약, 에러처리를 추가하고 싶다면 withThrowingTaskGroup(of:returning:body:) 를 사용하면 된다.

여기서 중요한 것은 type을 명시해주어야한다.

 

- of: child task가 리턴할 타입

- returning: Group Task가 리턴 할 타입.

 

만약 리턴할 타입이 없다? Void.self를 넣어주면 된다.

 

    let allResults = await withTaskGroup(of: (String, Double).self, // child task return type
                                         returning: [String: Double].self, // group task return type
                                         body: { taskGroup in

 

TaskGroup을 만들고 addTask(priority: operation:) 함수를 이용하면 Task를 추가 할 수 있다.

        // Loop through operations array
        for operation in operations {
            // Add child task to task group
            taskGroup.addTask {
                // Execute slow operation
                let value = await operation.execute()
                // Return child task result
                return (operation.name, value)
            }
        }

그림으로 보면 이런 느낌일 것이다.

이제 각 Task들은 독자적으로 실행이 되기 때문에 더이상 서로를 기다리지 않을 것이다.

Task Group의 결과를 리턴한다.

        // Collect results of all child task in a dictionary
        var childTaskResults = [String: Double]()
        for await result in taskGroup {
            // Set operation name as key and operation result as value
            childTaskResults[result.0] = result.1
        }

이제 코드를 실행해보면.. SleepDuration을 모두합하면 약 9초가 걸려야 하는데 5.36초만에 끝난 것을 확인 할 수 있다. 👍

 

 

참고.

https://swiftsenpai.com/swift/understanding-task-groups/

 

Understanding Swift Task Groups With Example - Swift Senpai

In this article, learn how to create a task group, add child tasks to a task group, and gather results from all the child tasks.

swiftsenpai.com

https://velog.io/@wansook0316/Task-TaskGroup

 

Task & TaskGroup

Task와 TaskGroup은 무엇일까? 그리고 Apple이 말하는 Structured Concurrency는 무엇일까.

velog.io

 

'Swift' 카테고리의 다른 글

Swift Concurrency 알아보기  (0) 2023.01.08
클래스의 초기화  (0) 2022.08.07
DiffableDataSource 알아보기.  (7) 2022.07.25
정규 표현식 in Swift  (2) 2022.06.14
Type Erasure in Swift  (0) 2022.05.23

댓글