Deep Dive to Swift Concurrency 시리즈
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개 출력 될까? -> ❌
Task Tree 특성으로 자식->부모로 에러가 전달
부모Task가 Error -> 자식Task 모두 cancel
자식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을 이용하면 다음을 얻을 수 있다.
Task Tree(부모Task / 자식 Task)가 만들어져서 만약 부모Task가 cancel된다면 자식도 cancel로 마킹이 된다.
group으로 묶어서 처리 이후에 해야할 작업들을 처리 할 수 있다.