구조적 동시성은 비동기 작업들이 명확한 부모-자식 관계와 생명주기를 가지도록 하는 프로그래밍 패러다임을 말한다.
"모든 비동기 작업은 정의된 범위(scope) 내에서 시작되고 종료되어야 한다"
마치 중괄호 {} 안에서 변수의 생명주기가 관리되는 것처럼, 비동기 작업도 구조적으로 관리된다.
장점들은 다음과 같다.
쉽게말해 주방장이 파스타 주문이라고 말하면 보조 요리사들이 각각 재료를 준비하고, 면을 삶고, 소스를 만들기를 함께 진행한다.
이후 모든 과정이 끝나면 주방장은 기다렸다 플레이팅을 한다.
병원진료처럼 누구나 접수를하고 진료를 받고 처방을 받고 약을 받는것처럼 누가 봐도 같은 방식으로 이해하고 사용할수 있는 표준이 있다는 것이다.
기존 병렬처리의 낮은 가독성과 달리 읽기쉬운 코드로 구현이 가능하다.
func fetch(num: Int) async throws -> UIImage? {
try await Task.sleep(nanoseconds: 1_000_000_000)
print("\(num) 함수")
return UIImage(named: "")
}
아래와 같은코드는 병렬 실행의 방법이 아니다 실제로 모든 print가 출력되는데 4초가 걸리는것을 볼 수 있다.
Task {
let data1 = try await fetch(num: 1) // 1초 대기
let data2 = try await fetch(num: 2) // 위에가 완료시 또 1초대기
let data3 = try await fetch(num: 3) // 반복...
let data4 = try await fetch(num: 4)
}
첫번째 병렬처리 방법은 async let 이다.
장점
단점
병렬처리의 갯수가 많지않거나ㅡ 작업개수를 미리 알고있거나, 코드 가독성이 중요할때 사용하는것이 좋다.
Task {
async let data1 = fetch(num: 1)
async let data2 = fetch(num: 2)
async let data3 = fetch(num: 3)
async let data4 = fetch(num: 4)
let datas = try await [data1, data2, data3, data4]
}
두번째 방법으로는 TaskGroup 이다.
장점
단점
배열이나 반복문같이 작업개수가 런타임에 결정될때, 많은 수의 작업을 병렬로 처리할 때 사용하는것이 좋다.
Task {
let datas = try await withThrowingTaskGroup(of: UIImage?.self) { group in
for i in 1...4 {
group.addTask {
try await fetch(num: i)
}
}
var results: [UIImage?] = []
for try await data in group {
results.append(data) // 완료된 순서대로 수집 (순서 보장 X)
}
return results
}
}
세번째 방법으로는 Task를 각자 생성하는것이다. 하지만 이건 병렬 처리는 맞지만 구조적 동시성은 아니다
장점
단점
부모와 생명주기를 끊고 싶을때와 같은 정말 특수한경우일때만 사용한다, 결과를 기다리지않고 성공실패에 관심없는 작업 등등..
Task {
let task1 = Task { try await fetch(num: 1) }
let task2 = Task { try await fetch(num: 2) }
let task3 = Task { try await fetch(num: 3) }
let task4 = Task { try await fetch(num: 4) }
let datas = try await [task1.value, task2.value, task3.value, task4.value]
}