Swift는 구조화된 방식으로 비동기(asynchronous)와 병렬(parallel) 코드 작성을 지원한다.
병렬 또는 비동기 코드는 추가 스케줄링의 유연성도 있지만 한편 복잡성이 증가하기도 한다.
async
키워드throws
전에 async
를 작성func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
await
키워드로 표시let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)
갤러리에 모든 사진의 이름을 가져온 다음 첫번째 사진을 보여주는 코드
listPhotos(inGallery:)
와 downloadPhoto(named:)
함수 모두 네트워크 요청을 필요로 하기 때문에 완료하는데 비교적 오랜시간이 걸릴 수 있다.
반환 화살표 전에 async
를 작성하여 둘 다 비동기로 만들면 이 코드는 그림이 준비될 때까지 기다리는 동안 앱의 나머지 코드가 계속 실행될 수 있다.
위 예제의 동시성을 이해하기 위한 실행 순서
1. 실행을 시작 ~ 첫번째 await
까지 실행된다. listPhotos(inGallery:)
함수를 호출하고 반환될 때까지 실행을 일시 중단한다.
2. 실행이 일시 중단되는 동안 같은 프로그램의 다른 코드가 동시에 실행된다. (예를 들어 오랜 시간 실행되어야 하는 백그라운드 작업인 새 사진의 목록 업데이트 등을 할 수 있다.) 이 코드는 await
로 표시된 다음 중단 지점 또는 완료될 때까지 실행된다.
3. listPhotos(inGallery:)
가 반환된 후에 이 코드는 해당 지점에서 시작하여 계속 실행된다. 반환된 값을 photoNames
에 할당한다.
4. sortedNames
와 name
을 정의하는 라인은 일반적인 동기 코드이다.
5. 다음 await
는 downloadPhoto(named:)
함수에 대한 호출을 표시한다.
6. downloadPhoto(named:)
가 반환된 후에 반환값은 photo
에 할당된 다음에 show(_:)
를 호출할 때 인수로 전달된다.
Swift가 현재 스레드에서 코드의 실행을 일시 중단하고 대신 해당 스레드에서 다른 코드를 실행하기 때문에 스레드 양보(yielding the thread)라고도 부른다.
await
가 있는 코드는 실행을 일시 중단할 수 있어야 하므로 프로그램의 특정 위치에서만 비동기 함수/메소드를 호출할 수 있다.
@main
으로 표시된 구조체, 클래스, 또는 열거형의 정적 (static) main()
메소드에 있는 코드
Task.sleep(_:)
메소드는 동시성 작동 방식을 위해 간단한 코드를 작성할 때 유용하다. 이 메소드는 아무런 동작도 하지 않지만 반환되기 전에 주어진 나노 단위의 초만큼 기다린다. 다음은 네트워크 작업 대기를 시뮬레이션하기 위해sleep(nanoseconds:)
을 사용하는listPhoto(inGallery:)
함수의 버전이다:func listPhotos(inGallery name: String) async throws -> [String] { try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // Two seconds return ["IMG001", "IMG99", "IMG0404"] }
이전 섹션에서는 비동기적으로 배열의 모든 요소가 준비된 후에 전체 배열을 한번에 반환했다.
또 다른 접근 방식은 비동기 시퀀스(asynchoronous sequence)를 사용하여 한번에 콜렉션의 한 요소를 기다리는 것이다.
import Foundation
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
}
for
- await
- in
루프는 다음 요소를 사용할 수 있을 때까지 기다리고 각 반복이 시작될 때 잠재적으로 실행을 일시 중단한다.
await
을 사용하여 비동기 함수를 호출하면 한번에 코드의 한 부분만 실행된다.
비동기 코드가 실행되는 동안 호출자는 코드의 다음 라인을 실행하기 위해 이동하기 전에 해당 코드가 완료될 때까지 기다린다.
예를 들어 다음 예제에서는 downloadPhoto(named:)
함수에 대해 세 번의 호출을 기다린다.
let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])
let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
이 방식으로 할 경우, 다운로드가 비동기이고 진행되는 동안 다른 작업을 수행할 수 있지만 downloadPhoto(named:)
에 대한 호출은 한번에 하나만 실행되는 단점이 있다.
비동기 함수를 호출하고 주변의 코드와 병렬로 실행하려면 상수를 정의할 때 let
앞에 async
를 작성하고 상수를 사용할 때마다 await
을 작성한다.
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
이 방식에서는 downloadPhoto(named:)
을 호출하는 세 가지는 모두 이전 호출이 완료되길 기다리지 않고 시작된다. 코드가 함수의 결과를 기다리기 위해 일시 중단되지 않기 때문에 await
로 표시하지 않고, photos
가 정의된 라인까지 계속 실행된다.
두 접근의 차이점:
await
를 사용하여 비동기 함수를 호출한다. 이는 순차적으로 실행되는 작업을 생성한다.async
-let
을 사용하여 비동기 함수를 호출한다. 이렇게 하면 병렬로 수행할 수 있는 작업이 생성된다.await
과 async
-let
은 모두 일시 중단되는 동안 다른 코드를 실행할 수 있도록 한다.await
로 표시한다.작업이란?
구조화되지 않은 작업
Task.init(priority:operation:)
초기화 구문 호출Task.detached(priority:operation:)
클래스 메소드 호출let newPhoto = // ... some photo data ...
let handle = Task {
return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value
Swift 동시성은 협동 취소 모델(cooperative cancellation model)을 사용한다.
CancellationErro
와 같은 에러 발생nil
또는 빈 콜렉션 반환취소를 확인
Task.checkCancellation()
을 호출하거나Task.isCancelled
의 값을 확인하고 자체 코드에서 취소를 처리함취소를 수동으로 전파하려면 Task.cancel()
을 호출해야 함
온도를 기록하는 행위자 예제
actor TemperatureLogger {
let label: String
var measurements: [Int]
private(set) var max: Int
init(label: String, measurement: Int) {
self.label = label
self.measurements = [measurement]
self.max = measurement
}
}