티스토리 뷰
[1] TaskGroup과 ThrowingTaskGroup
: A group that contains dynamically created child tasks. (자세한 설명은 이 글 5번 참고)
taskGroup을 만드려면
withTaskGroup(of:returning:body:) 를 호출.
: A group that contains throwing, dynamically created child tasks.
throwing task group을 만드려면
withThrowingTaskGroup(of:returning:body:) 를 호출
[2] 사용예제
2.1 간단한 병렬 실행을 위해 async-let syntax 를 쓰다가 확장이 필요할 때
여러장의 이미지를 병렬로 fetch 해와야하는 상황이라고 해봅시다.
저는 성공한 케이스들만 UIImage 배열로 리턴해줄 것 입니다.
이미지 fetch 메소드는 아래와 같고 (여기서 코드참고했어요)
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)
if let image = UIImage(data: data) {
return image
} else {
throw URLError(.badURL)
}
} catch {
throw error
}
}
예를들어 2개의 연산 정도 되는 간단한 병렬 실행이면 async let 쓰겠지만..
func fetchImages() async -> [UIImage] {
async let fetchImage1 = fetchImage(urlString: "https://picsum.photos/300")
async let fetchImage2 = fetchImage(urlString: "https://picsum.photos/300")
let (image1, image2) = await (try? fetchImage1, try? fetchImage2)
return [image1, image2].compactMap { $0 }
}
이미지 여러개를 가져오도록 확장되었을 때는 async-let syntax 를 쓰기 힘들어집니다;;;
async let fetchImage1 = fetchImage(urlString: "https://picsum.photos/300")
async let fetchImage2 = fetchImage(urlString: "https://picsum.photos/300")
async let fetchImage3 = fetchImage(urlString: "https://picsum.photos/300")
async let fetchImage4 = fetchImage(urlString: "https://picsum.photos/300")
async let fetchImage5 = fetchImage(urlString: "https://picsum.photos/300")
let (image1, image2, image3, ...) = await (try? fetchImage1, try? fetchImage2, ...)
이 때 TaskGroup을 씁니다.
async-let syntax 도 내부적으로 child task 를 생성하는 데, 이와 동일하게 직접 child task를 생성하는 셈입니다.
에러를 던지지 않을 것이니까 withTaskGroup을 사용해줍니다.
메소드 시그니쳐가 좀 복잡한데 두개만 잘 체크해주면 됩니다.
- of 는 ChildTask 의 결과 타입
- body는 group -> GroupResult
(returing 은 작성해도 되지만, 컴파일러에 의해 잘 추론됨)
func withTaskGroup<ChildTaskResult, GroupResult>(
of childTaskResultType: ChildTaskResult.Type,
returning returnType: GroupResult.Type = GroupResult.self,
body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult where ChildTaskResult : Sendable
먼저 withTaskGroup 메소드로 TaskGroup을 만들고
addTask(priority:operation:) 로 group 에 childTask 를 추가합니다.
func fetchImages() async -> [UIImage] {
let urlStrings = [
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300"
]
// return 해주는 게 없으니 returing 타입은 void 로 추론됨.
let images = await withTaskGroup(of: UIImage?.self) { group in
for urlString in urlStrings {
group.addTask {
try? await self.fetchImage(urlString: urlString)
}
}
}
return []
}
이제 GroupResult (이미지 배열) 를 리턴해주는 코드를 추가합니다.
func fetchImages() async -> [UIImage] {
let urlStrings = [
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300"
]
// [UIImage] 를 리턴하므로 returing 타입은 [UIImage] 로 추론됨.
let images = await withTaskGroup(of: UIImage?.self) { group in
for urlString in urlStrings {
group.addTask {
try? await self.fetchImage(urlString: urlString)
}
}
var images: [UIImage] = []
for await image in group {
if let image {
images.append(image)
}
}
return images
}
return images
}
참고로 TaskGroup은 AsyncSequence 를 conform 하고 있기 때문에
reduce 를 사용해서 간결하게 코드를 작성할 수도 있습니다.
func fetchImages() async -> [UIImage] {
let urlStrings = [
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300"
]
let images = await withTaskGroup(of: UIImage?.self) { group in
for urlString in urlStrings {
group.addTask {
try? await self.fetchImage(urlString: urlString)
}
}
return await group
.reduce(into: [UIImage?](), { $0.append($1) })
.compactMap { $0 }
}
return images
}
최종코드
func fetchImages() async -> [UIImage] {
let urlStrings = [
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300"
]
return await withTaskGroup(of: UIImage?.self) { group in
for urlString in urlStrings {
group.addTask {
try? await self.fetchImage(urlString: urlString)
}
}
return await group
.reduce(into: [UIImage?](), { $0.append($1) })
.compactMap { $0 }
}
}
[3] Child Task 의 결과에 즉각 반응하기
아래 내용은 'Modern Concurrency in Swift (chapter 7)' 에 나오는 내용을 참고했습니다.
TaskGroup 은 동적으로 group 의 workload 를 매니징할 수 있게 해줍니다.
즉 실행 중에 새로운 task 를 추가하거나 취소하는 것이 가능하고
각 task 의 결과에 따른 즉각적인 업데이트도 가능합니다.
(예를들어 progress UI 를 점진적으로 업데이트 해야한다던가.. 각 task 의 결과를 보고 group execution 을 컨트롤해야한다던가.. )
체크한 부분에 해당 코드를 추가하면 됩니다.
예를들어 각 작업의 진행상황을 즉각적으로 알 수 있는 코드 입니다.
func fetchImages() async {
let urlStrings = [
"https://picsum.photos/100",
"https://picsum.photos/200",
"https://picsum.photos/300",
"https://picsum.photos/400",
"https://picsum.photos/500"
]
await withTaskGroup(of: UIImage?.self) { group in
for urlString in urlStrings {
group.addTask {
try? await self.fetchImage(urlString: urlString)
}
}
✅
for await image in group {
print("Completed:", image)
}
print("Done.")
}
}
// 출력결과
Completed: Optional(<UIImage:0x6000033100a0 anonymous {200, 200} renderingMode=automatic(original)>)
Completed: Optional(<UIImage:0x6000033140a0 anonymous {100, 100} renderingMode=automatic(original)>)
Completed: Optional(<UIImage:0x600003301040 anonymous {300, 300} renderingMode=automatic(original)>)
Completed: Optional(<UIImage:0x600003301040 anonymous {400, 400} renderingMode=automatic(original)>)
Completed: Optional(<UIImage:0x600003309860 anonymous {500, 500} renderingMode=automatic(original)>)
Done.
상황에 따라 이 쪽에 addTask 나 cancel 코드를 넣을 수도 있겠습니다.
[4] TaskGroup 의 Cancellation behavior
TaskGroup 은 위에서 사용하던 addTask 뿐만아니라 addTaskUnlessCancelled 메소드도 제공합니다.
이 메소드의 필요성을 파악하기 위해, 우선 TaskGroup 이 취소되는 상황을 살펴봅시다.
- cancelAll()이 호출될 때
- 이 TaskGroup 을 실행하는 Task가 취소될 때 (예를 들어 아래와 같은 상황의 Task)
let task = Task {
await withTaskGroup(of:) { .. }
}
(참고로 TaskGroup은 structured concurrency 에 해당하는 타입이므로, TaskGroup 을 취소하면 모든 child tasks 들에게 자동으로 취소가 전파됩니다.)
취소된 TaskGroup은 여전히 Task 를 새로 추가할 수 있지만, 추가된 Task 는 즉시 취소되기 시작합니다.
이 때, 이미 취소된 TaskGroup에는 새 Task 를 추가하지 않고 싶다! 하면 조건 없이 작업을 추가하는 addTask 대신, addTaskUnlessCancelled 를 사용합니다.
이 메소드는 리턴값도 있는데 task 가 그룹에 추가되었으면 true, 아니면 false 를 반환합니다.
[ 추천 글 ]
잘 정리되어있음 👍
https://liam777.tistory.com/41
'🍏 > Swift' 카테고리의 다른 글
[Swift] actor (0) | 2023.04.10 |
---|---|
[Swift] Sendable (0) | 2023.04.08 |
[Swift] Understanding Swift Performance 를 보면서 알게 된 것 (0) | 2023.03.04 |
[Swift Collections] deque (1) | 2022.10.08 |
[Swift] some, any 키워드 (2) | 2022.07.16 |
- Total
- Today
- Yesterday
- 장고 URL querystring
- 플러터 얼럿
- Dart Factory
- ribs
- 장고 Custom Management Command
- PencilKit
- 플러터 싱글톤
- Django Heroku Scheduler
- 구글 Geocoding API
- flutter 앱 출시
- cocoapod
- Django FCM
- Flutter 로딩
- METAL
- ipad multitasking
- Watch App for iOS App vs Watch App
- github actions
- SerializerMethodField
- flutter build mode
- flutter deep link
- DRF APIException
- flutter dynamic link
- Flutter Clipboard
- drf custom error
- Flutter Spacer
- Flutter getter setter
- Django Firebase Cloud Messaging
- Python Type Hint
- Flutter Text Gradient
- Sketch 누끼
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |