
이것도 하면서 저것도 하고, 한 번에 여러 일을 수행하는 것을 멀티 태스킹이라고 한다.
태스크는 무엇인가?

사전적 정의는 일단 '(특히 힘들거나 하기 싫은) 일'이라고 한다.
어쩐지 멀티 태스킹하는 사람들이 그렇게 힘들어하더라니
그렇다면 Swift의 Task는 어떤 일인지 알아보자.
A task is a unit of work that can be run asynchronously as part of your program.
Task는 비동기적으로 실행될 수 있는 작업 단위라고 한다.
Task 자체는 한 번에 한 가지 작업만 수행하지만 여러 작업을 생성하면 Swift는 해당 작업이 동시에 실행되도록 예약할 수 있다. 즉, 실생활의 멀티 태스킹과 비슷하다!
async-let이전 포스팅에서 설명한 async-let 구문은 암시적으로 하위 작업을 생성한다.
A group that contains dynamically created child tasks.
작업에 대한 그룹을 만들어서 동적으로 하위 작업을 추가할 수 있는데, 이 타입을 사용하면 우선 순위나 작업의 취소를 더 손쉽게 관리할 수 있다.
하위 작업이라는 표현에서 눈치 챈 사람도 있겠지만, 작업은 계층 구조를 이루게 된다.
지정된 작업 그룹의 각 작업에는 동일한 상위 작업이 있으며 각 작업에는 하위 작업이 있을 수 있다.
작업과 작업 그룹 간의 명시적인 관계로 인해 이 접근 방식을 구조적 동시성(structured concurrency)이라고 한다.
상위 작업에서는 하위 작업이 완료될 때까지 기다려야 한다.
하위 작업에 더 높은 우선순위를 설정하면 상위 작업의 우선순위가 자동으로 높아진다.
상위 작업이 취소되면 각 하위 작업도 자동으로 취소된다.
Task-local 값은 하위 작업에 효율적이고 자동으로 전파된다.
func cookFoods() async -> [String] {
let foods = await withTaskGroup(of: String.self) { group in
let foods = Food.allCases
for food in foods {
group.addTask {
return await food.cook()
}
}
var results = [String]()
for await food in group {
results.append(food)
}
return results
}
return foods
}
for _ in 0..<5 {
print(await cookFoods())
}
// ["파스타", "스테이크", "리조또"] 출력
// ["스테이크", "리조또", "파스타"] 출력
// ["스테이크", "리조또", "파스타"] 출력
// ["파스타", "스테이크", "리조또"] 출력
// ["스테이크", "리조또", "파스타"] 출력
작업 그룹을 만들고 하위 작업을 추가할 때 하위 작업의 완료 순서가 보장되지 않기 때문에 위의 예시에서는 반복문이 실행될 때마다 다른 결과가 나왔다.
만약 오류가 발생할 수 있으면 withThrowingTaskGroup(of:returning:body:)를 사용한다.


Swift Concurrency는 취소 모델을 사용하기 때문에 비동기 작업을 취소할 수도 있다.
각 작업은 실행 중 적절한 지점에서 취소되었는지 여부를 확인하고 적절하게 취소에 응답한다.
CancellationError와 같은 오류 발생
nil 또는 빈 컬렉션 반환
부분적으로 완료된 작업 반환
가령 이미지가 크거나 네트워크 속도가 느린 경우 이미지를 다운로드한다고 해보자.

시간이 오래 걸릴 수도 있다.

기다림에 지친 유저는 그냥 취소해버릴 수도 있으니, 작업을 중지할 수 있어야 한다.
작업을 중지할 수 있도록 하려면 작업이 취소되었는지 확인하고 취소된 경우 실행을 중지해야 한다.
취소되었는지 확인하는 방법은 두 가지가 있다.

취소되었는지 확인하는 이유는 이미 취소된 작업에 대해 checkCancellation()을 호출하면 오류가 발생하기 때문이다.
그 밖에도 오류를 발생시킬 수 있는 작업은 오류를 작업 외부로 전파하여 모든 작업을 중지할 수 있다.

이는 구현과 이해가 간단하다는 장점이 있다.
유연성을 높이려면 isCancelled 프로퍼티를 쓰면 된다. 이 프로퍼티를 사용하면 네트워크 연결 닫기, 임시 파일 삭제 등 작업 중지의 일부로 정리 작업을 수행할 수 있다는 소문이...아니라 공식 문서에 나온 오피셜이다.
let task = await Task.withTaskCancellationHandler {
// ...
} onCancel: {
print("취소!")
}
만약 이런 코드가 있다고 하자.

task.cancel() // "취소!" 출력
작업을 취소하면 onCancel에 전달한 정리 작업이 수행된다.
취소를 요청하더라도 바로 모든 작업이 중지되는 것은 아니라 이 취소에 대한 정리 작업 클로저(일명 취소 핸들러)가 실행될 때 아직 작업이 수행중일 수 있으므로 레이스 컨디션일 수 있는 정리 작업은 하지 않아야 한다.
오늘은 태스크에 대해 알아보았다.
더는 못 기다려서 취소하니까 막 작업이 완료된 경우 상당히 어색한 눈물을 흘릴지도 모른다는 생각이 들었다.
