
(※ hackingWithSwift의 글을 번역한 것으로 아래 출처를 남겨두었습니다. 약간의 오역이 있을 수 있으니 지적해주시면 감사드리겠습니다.)
직역하자면 구조화된 동시성이다. async/await와 async시퀀스 작업에 concurrent operation을 실행, 취소, 관찰할 수 있는 것을 도입했다고 한다.
예제로 살펴보는 것이 빠르겠다.
enum LocationError: Error {
case unknown
}
func getWeatherReadings(for location: String) async throws -> [Double] {
switch location {
case "London":
return (1...100).map { _ in Double.random(in: 6...26) }
case "Rome":
return (1...100).map { _ in Double.random(in: 10...32) }
case "San Francisco":
return (1...100).map { _ in Double.random(in: 12...20) }
default:
throw LocationError.unknown
}
}
func fibonacci(of number: Int) -> Int {
var first = 0
var second = 1
for _ in 0..<number {
let previous = first
first = second
second = previous + first
}
return first
}
하나는 지역에 따라 특정 숫자를 반환하는 비동기 작업이고, 나머지는 익히 본 피보나치 수열에서 특정 숫자를 반환하는 동기 작업이다.
이 때, @main 속성을 사용하고, 비동기 함수 앞에 main() 메소드를 함께 사용하면 곧장 비동기 맥락,환경(async context)로 가게끔 할 수 있다는 것이 structured concurrency에 의해 도입된 가장 심플한 것이다.
@main
struct Main {
static func main() async throws {
let readings = try await getWeatherReadings(for: "London")
print("Readings are: \(readings)")
}
}
structured concurrency의 가장 주요한 변화라고 한다. 동시성 연산을 각각 혹은 조합해서 사용할 수 있게 한다고 하는데..
단순하게 표현하자면 새로운 Task 객체를 생성하여 동시성 작업(concurrent work)을 시작할 수 있고 이것을 원하는 연산에 전달해줄 수 있다는 것이다. 그러면 이 작업은 백그라운드 스레드에서 돌기 시작할 것이고, 우리는 최종 값이 리턴될 때까지 await를 사용해서 기다릴 것이다.(즉, 다른 작업을 할 수 있으면서 값이 오는 것을 기다릴 수 있다.)
이것도 예제로 보는 것이 빠를 것 같다.
func printFibonacciSequence() async {
let task1 = Task { () -> [Int] in
var numbers = [Int]()
for i in 0..<50 {
let result = fibonacci(of: i)
numbers.append(result)
}
return numbers
}
let result1 = await task1.value
print("The first 50 numbers in the Fibonacci sequence are: \(result1)")
}
우리가 위 함수의 결과를 얻기 위해 백그라운드 스레드에서 함수를 재귀적으로 호출될 것을 압니다. Task { () -> [Int] in ... } (혹은 Task {...} 로 표현) 를 명시함으로써 swift는 작업이 리턴될 것을 인지하게 된다.
Tip: 참고로 class나 struct 내에서 Task를 쓸 때, 속성이나 메소드에 접근하기 위한 self를 명시하지 않아도 된다고 합니다. 왜냐하면, 작업은 나중을 위해 연산을 저장하는 것 이 아니라 바로 실행되는 non-escaping closure이기 때문입니당. 반면, escaping closure는 값을 저장해두거나 비동기 작업을 할 때 사용되며 self를 명시하라고 합니다. https://www.c-sharpcorner.com/article/what-is-escaping-and-non-escaping-closure-in-swift/ esacping closure 과 non-esacping closure에 대한 자료입니다.
func runMultipleCalculations() async throws {
let task1 = Task {
(0..<50).map(fibonacci)
}
let task2 = Task {
try await getWeatherReadings(for: "Rome")
}
let result1 = await task1.value
let result2 = try await task2.value
print("The first 50 numbers in the Fibonacci sequence are: \(result1)")
print("Rome weather readings are: \(result2)")
}
let result1 = await task1.value의 'value'는 Error를 자동으로 던질 수 있는 프로퍼티..!! 결론적으로 우리는 Task와 value 프로퍼티를 사용함으로써 두 작업을 동시에 수행하고 완료되기를 기다릴 수 있게 된다.
추가적으로, Task의 우선순위를 정할 수 있다고 한다! 종류로는 high, default, low, background가 있고 Task(priority:)에 넣어주면 된다. 명시하지 않으면 당연히 default로 설정! userInteractive는 메인 스레드를 위한 것으로 여기서는 불가능하다.
코드가 실행되는 것을 통제할 수 있는 몇 가지 정적 함수도 존재하는데,
1. Task.sleep() : 말 그대로 현재 작업을 해당 시간동안 sleep 시킴
2. Task.checkCancellation() : 어디선가 cancel() 메소드를 호출해서 해당 작업을 취소시켰는지 확인하는 것이다. 만약 cancel()로 해당 작업을 호출하고자 했다면 CancellationError를 던지는 것이다.
3. Task.yield() : 기다리고 있을지 모르는 다른 작업을 위해 현재 작업이 잠시 양보, 중단하는 것이다. loop로 집약적인 작업 중일 때 중요하다
다음은 1,2번을 사용한 예시이다.
func cancelSleepingTask() async {
let task = Task { () -> String in
print("Starting")
await Task.sleep(1_000_000_000)
try Task.checkCancellation()
return "Done"
}
// The task has started, but we'll cancel it while it sleeps
task.cancel()
do {
let result = try await task.value
print("Result: \(result)")
} catch {
print("Task was cancelled.")
}
}
분명 중간에, task.cancel()이 호출되어 CancellationError를 즉시 던질 것이지만, task.value가 호출될 때 우리에게 CancellationError가 나타난다.
Tip: task.value가 아닌 task.result를 사용하면 작업의 성공/실패 여부를 포함한 Result 타입으로 얻을 수 있다.
좀 더 복잡한 작업의 경우, TaskGroup을 사용한다. 리스크를 줄이기 위해 public initializer를 사용하는 것이 아닌, withTaskGroup() 함수를 사용해서 task group을 만든다.
※주의: withTaskGroup() 밖에서 task group을 복사하는 행위는 절.대. 금지라고 한다.
예시를 살펴볼 시간입니당
func printMessage() async {
let string = await withTaskGroup(of: String.self) { group -> String in
group.async { "Hello" }
group.async { "From" }
group.async { "A" }
group.async { "Task" }
group.async { "Group" }
var collected = [String]()
for await value in group {
collected.append(value)
}
return collected.joined(separator: " ")
}
print(string)
}
결과적으로 "Hello", "From", "A", "Task", "Group" 이 조합되어 나오지만 비동기적으로 처리되기 때문에 순서가 뒤집혀서 나올 가능성이 있다.
Tip: task group은 모두 같은 타입을 반환해야 하므로, enum을 사용하는 것을 추천한다. 좀 더 심플한 대안은 뒤에서 살펴볼 Async Let Binding이다.
에러를 핸들링에 관해,
1. task group 내에서 에러를 처리하는 법
2. task group 밖에서 에러를 처리하는 법 ->withThrowingTaskGroup() + try
func printAllWeatherReadings() async {
do {
print("Calculating average weather…")
let result = try await withThrowingTaskGroup(of: [Double].self) { group -> String in
group.async {
try await getWeatherReadings(for: "London")
}
group.async {
try await getWeatherReadings(for: "Rome")
}
group.async {
try await getWeatherReadings(for: "San Francisco")
}
// Convert our array of arrays into a single array of doubles
let allValues = try await group.reduce([], +)
// Calculate the mean average of all our doubles
let average = allValues.reduce(0, +) / Double(allValues.count)
return "Overall average temperature is \(average)"
}
print("Done! \(result)")
} catch {
print("Error calculating data.")
}
}
위 코드는 async()를 각각 호출했지만 location in ["London", "Rome", "San Francisco"] { to call async()... } 이런 식으로 쓰면 좀 더 간결하다.
TaskGroup의 메소드로,
cancelAll()은 모든 작업을 취소시킬 수 있지만, 이후에 async() 가 나타난다면 그룹에 작업을 추가할 것이다. 그 대안은asyncUnlessCancelled()를 사용하는 것으로 그룹이 cancel되면 더 이상 작업을 추가하지 않게 할 수 있다.
동시성을 가능하게 하는 Task에 대해 알아보았고 다음에는 async let binding과 동기 코드로 비동기 작업에 결합시키는 것에 대해 알아보도록 하자..
출처: https://www.hackingwithswift.com/articles/233/whats-new-in-swift-5-5