Deep Dive to Swift Concurrency - task group

Deep Dive to Swift Concurrency - task group

structed concurrency

Deep Dive to Swift Concurrency 시리즈

  1. https://toby.hashnode.dev/deep-dive-to-swift-concurrency-async-let

Task Group

이전 시간 async let을 활용하여 Structed Concurrency(부모Task, 자식Task)를 구현해보았음.

func fetchOneThumbnail(withID id: String) async throws -> UIImage {
    let imageReq = imageRequest(for: id)
    let metadataReq = metadataRequest(for: id)
    async let (data, _) = URLSession.shared.data(for: imageReq) // 1️⃣
    async let (metadata, _) = URLSession.shared.data(for: metadataReq) // 2️⃣
    guard let size = parseSize(from: try await metadata),
          let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size)
    else {
        throw ThumbnailFailedError()
    }
    return image
}

하지만 fetchOneThumbnail 을 여러개 불러야한다면? 여러개 부른 값을 저장해야 한다면?

이 때 등장한 것이 Task Group.

Task Group을 통해서 말 그대로 Task를 Group화 하고 group화 한 결과 값을 처리 할 수 있다.

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    for id in ids {
        thumbnails[id] = try await fetchOneThumbnail(withID: id)
    }
    return thumbnails
}

위와 같이 코드를 짠다고 했을 때 썸네일을 가져오는 함수가 concurency 할까? -> ❌

썸네일을 하나 하나 씩 가져올 것이다.

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    try await withThrowingTaskGroup(of: Void.self) { group in
        for id in ids {
            group.addTask {
                // ❌ Error: Mutation of captured var 'thumbnails' in concurrently executing code
                thumbnails[id] = try await fetchOneThumbnail(withID: id)
            }
        }
    }
    return thumbnails
}

그래서 등장한 것이 Task Group 이다. (Apple Developer Doc)

of 뒤에는 Type을 쓰며, 각 Task의 return 값을 쓰게 된다.

withThrowingTaskGroup(of: 원하는 반환 값 타입)

하지만 에러가 뜬다. 왜냐하면 여러개의 Task가 Mutable value에 접근하면

Data race가 일어날 수 있기 때문이다.

그럼 어찌 수정함?

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
        for id in ids {
            group.async {
                return (id, try await fetchOneThumbnail(withID: id))
            }
        }
        // Obtain results from the child tasks, sequentially, in order of completion.
        for try await (id, thumbnail) in group {
            thumbnails[id] = thumbnail
        }
    }
    return thumbnails
}

다음과 같이 수정할 수 있다.

  • Child Task를 (ID UIImage) 튜플을 반환하게 한다.

  • 연속성을 보장해주는 for - await를 사용하여 Data race 상황을 피한다.

Task Group의 Task Tree 속성 예시

그럼 Task Tree의 특성이 유지되는지 확인해볼까?

Task {
    try await withThrowingTaskGroup(of: Int.self) { group in
        let arr = [0, 0, 1, 0, 0]

        for num in arr {
            group.addTask {
                if num == 1 {
                    throw CustomError.myError
                }
                return num
            }
        }
        for try await num in group {
            print(num)
        }
        print("END")
    }
}

과연 0이 4개 출력 될까? -> ❌

  1. Task Tree 특성으로 자식->부모로 에러가 전달

  2. 부모Task가 Error -> 자식Task 모두 cancel

  3. 자식Task가 num을 반환 했어도 쓸모가 없어졌다.

Task {
    try await withThrowingTaskGroup(of: Int.self) { group in
        let arr = [0, 0, 1, 0, 0]

        for num in arr {
            group.addTask {
                if num == 1 {
                    throw CustomError.myError
                }
                print(num) // ✨✨
                return num
            }
        }
        for try await num in group {
            // print(num)
        }
        print("END")
    }
}

그럼 이렇게 자식Task에 print을 옮긴 경우는 0 이 4번 출력될까? -> ✅

왜냐하면 Task Tree에 따라서 자식Task를 멈추는 것이 아닌 cancel 마크만 한다.

그렇기에 자식Task는 종료되지 않아서 0 이 4번 출력된다.

그 밖에도

(Apple Developer Doc) 에 들어가면 withThrowingTaskGroup 외에

Task Group이 많으니 참고 바람

정리

Task Group을 이용하면 다음을 얻을 수 있다.

  1. Task Tree(부모Task / 자식 Task)가 만들어져서 만약 부모Task가 cancel된다면 자식도 cancel로 마킹이 된다.

  2. group으로 묶어서 처리 이후에 해야할 작업들을 처리 할 수 있다.


https://developer.apple.com/wwdc21/10134