TaskGroup
TaskGroup은 동적으로 생성되는 자식 task 들을 포함할 수 있는 그룹입니다
task group을 만들기 위해서는 withTaskGroup(of: returning: body:) 메서드를 호출해야합니다.
Task execution order
task group 에 추가된 Task 들은 동시에 실행되며 무작위 순서로 수행될 수 있습니다.
Cancellation behavior
task group 은 다음과 같은 이유들로 취소될 수 있습니다
* cancelAll() 이 호출되었을 때
* 만약 Task 를 수행하고 있는 task group이 취소되었을 경우
TaskGroup는 구조적 동시성(Structured Concurrency)의 기본 구조로, 그 하위 작업들과 그 하위 작업들의 하위 작업들까지 모두 자동으로 취소가 전파됩니다.
취소된 작업 그룹은 여전히 새 작업을 추가할 수 있지만, 그 작업들은 즉시 취소됩니다. 이미 취소된 작업 그룹에 새 작업을 추가하지 않으려면, 조건 없이 작업을 추가하는 대신에 addTaskUnlessCancelled(priority:body:) 를 사용합니다. 이 메서드는 작업 그룹이 취소되지 않은 경우에만 작업을 추가하며, 그렇지 않은 경우 false 를 반환하여 명시적으로 취소 상태를 처리할 수 있게 합니다. 이미 취소된 TaskGroup 에 불필요한 작업을 추가하지 않고 취소 상태를 명확하게 다룰 수 있도록 도움이 됩니다.
withThrowingTaskGroup
withThrowingTaskGroup은 모든 자식 task들이 완료될 때까지 기다린 후 반환됩니다
withTaskGroup 외에도 withThrowingTaskGroup 함수가 존재합니다. 이 group은 모든 자식 task들이 완료될 때까지 기다린 후 반환됩니다. 심지어 취소된 작업들도 이 함수가 반환되기 전에 완료되어야 합니다. 취소된 하위 작업들은 협력적으로 취소에 반응하고 가능한 한 빨리 반환을 시도합니다. 이 함수가 반환되므로 이후에는 작업 그룹이 항상 비어있습니다.
자식 작업들로부터 받은 그룹의 반환값들은 다음처럼 저장될 수 있습니다.
만약 더 많은 컨트롤을 해야하거나 또는 적은 결과값들을 처리해야 하는 경우 next() 메서드를 직접 호출할 수 있습니다.
Task Group Cancellation
취소된 작업 그룹은 여전히 새 작업을 추가할 수 있지만, 그 작업들은 즉시 취소되며 이에 따라 행동할 것입니다. 이미 취소된 작업 그룹에 새 작업을 추가하지 않으려면, 조건 없이 작업을 추가하는 대신에 addTaskUnlessCancelled(priority:body:) 를 사용합니다. 이 메서드는 작업 그룹이 취소되지 않은 경우에만 작업을 추가하며, 그렇지 않은 경우 false 를 반환하여 명시적으로 취소 상태를 처리할 수 있게 합니다. 이미 취소된 TaskGroup 에 불필요한 작업을 추가하지 않고 취소 상태를 명확하게 다룰 수 있도록 도움이 됩니다.
Error Handling
하나의 자식 task에서 에러가 Throwing 된다고 바로 그룹에 있는 다른 작업들을 취소하는 것은 아닙니다. 하지만 withThrowingTaskGroup 메서드의 본문에서 예외를 throw하는 것은 그룹과 그 하위 작업 전체를 취소합니다. 예를 들어, task 그룹에서 next() 를 호출하고 해당 오류를 전파하면 다른 모든 작업이 취소됩니다. 아래의 코드에서는 아무것도 취소되지 않고 그룹에서 오류가 발생하지 않습니다:
반대로 아래 예제는 SomeError를 throw 하여 group 내부에 있는 모든 작업을 취소합니다.
개별 작업의 Group.next()에 대한 해당 호출에서 오류를 발생시켜 개별 오류를 처리하거나 그룹이 오류를 다시 던지도록 할 수 있는 기회를 제공합니다.
withThrowingTaskGroup 코드
다음은 Task 내부에 withThrowingTaskGroup을 사용하여 여러개의 Task를 group으로 만든것 입니다. 이후 addTask 를 통해 작업을 추가합니다, 이렇게 withTaskGroup을 사용하게 되면 하나의 group에 여러개의 dynamic한 개수의 task들을 만드는게 가능해집니다.
private func fetchImage(urlString: String) async throws -> UIImage {
guard let url = URL(string: urlString) else {
throw URLError(.badURL)
}
do {
let (data, _) = try await URLSession.shared.data(from: url, delegate: nil)
if let image = UIImage(data: data) {
return image
} else {
throw URLError(.badURL)
}
} catch {
throw error
}
}
func fetchImagesWithTaskGroup() async throws -> [UIImage] {
// 여러개의 group은 각각 UIImage를 반환한다는 의미
return try await withThrowingTaskGroup(of: UIImage.self) { group in
var images: [UIImage] = []
group.addTask {
try await self.fetchImage(urlString: "https://picsum.photos/300")
}
group.addTask {
try await self.fetchImage(urlString: "https://picsum.photos/300")
}
group.addTask {
try await self.fetchImage(urlString: "https://picsum.photos/300")
}
group.addTask {
try await self.fetchImage(urlString: "https://picsum.photos/300")
}
// 위에 여러개의 group.addTask로 받은 UIImage들은 모두 group에 저장되어 있어 순환하며 추가합니다.
for try await image in group {
images.append(image)
}
return images
}
}
또는 아래 코드처럼 클린하게 만들 수 있습니다.
func fetchImagesWithTaskGroup() async throws -> [UIImage] {
let urlStrings = [
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300"
]
// 여러개의 group은 각각 UIImage를 반환한다는 의미
return try await withThrowingTaskGroup(of: UIImage.self) { group in
var images: [UIImage] = []
for urlString in urlStrings {
group.addTask {
try await self.fetchImage(urlString: urlString)
}
}
for try await image in group {
images.append(image)
}
return images
}
}
하지만 여기서 5개의 이미지를 가져오도록 했는데 5개 중에서 4개의 이미지만 다운받아지고 나머지 1개의 이미지가 에러가 발생한다면 해당 에러로 인해서 다른 이미지들이 사용되지 못합니다. 그렇기 때문에 아래 코드처럼 try?로 명시하여 가져올 데이터를 ? optional로 명시해주고 if let으로 데이터가 존재하는 경우에만 추가되도록 만듭니다.
func fetchImagesWithTaskGroup() async throws -> [UIImage] {
let urlStrings = [
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300"
]
// 여러개의 group은 각각 UIImage를 반환한다는 의미
return try await withThrowingTaskGroup(of: UIImage?.self) { group in
var images: [UIImage] = []
for urlString in urlStrings {
group.addTask {
try? await self.fetchImage(urlString: urlString)
}
}
for try await image in group {
if let image = image {
images.append(image)
}
}
return images
}
}
Error Handling 증명
위 Error Handling 부분에서 만약 withThrowingTaskGroup에서 group이 에러가 발생한다고 가정했는데 이를 직접 테스트해보겠습니다.
만약 위에 코드에서 group.addTask 부분은 하나의 group에서 모든 url에 대한 작업을 수행하고 있는 것 입니다. 그렇기 때문에 만약 url을 하나라도 바꾸게 된다면 같은 group에서 throw가 발생하므로 모든 group내 작업들이 취소되어 이미지를 가져오는 모든 작업을 취소하게 됩니다.
for urlString in urlStrings {
group.addTask {
try? await self.fetchImage(urlString: urlString)
}
}
func fetchImagesWithTaskGroup() async throws -> [UIImage] {
let urlStrings = [
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos1/300"
]
// 여러개의 group은 각각 UIImage를 반환한다는 의미
return try await withThrowingTaskGroup(of: UIImage.self) { group in
var images: [UIImage] = []
for urlString in urlStrings {
group.addTask {
try await self.fetchImage(urlString: urlString)
}
}
for try await image in group {
images.append(image)
}
return images
}
}
'SwiftUI' 카테고리의 다른 글
Chart Proxy (0) | 2023.12.14 |
---|---|
DownLoading Image from server (AsyncImage) (0) | 2023.12.14 |
LazyV(H)Grid (0) | 2023.12.13 |
DisclosureGroup (0) | 2023.12.13 |
Form - 설정 메뉴로 주로 사용되는 container (0) | 2023.12.13 |