continuation은 기존의 애플의 레거시 콜백 기반 비동기 API나, 델리게이트 API들을 async/await 패턴으로 연결하는 브릿지 연결을 하는 객체이다.
안전 검사가 포함된 continuation인 CheckedContinuation와
성능 최적화를 위한 안전 검사가 없는 continuation UnsafeContinuation가 있다.
기존의 콜백 기반 API가 있다고 해보자.
보통 이런함수들은 애플이 미리 만들어 둔것이고, 아직 async/await을 지원해주지 않은 것들이다.
이러한 API들을 어떻게 async/await 방식으로 전환할수 있는지 알아보자
func oldAPI(completion: @escaping (String) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
completion("end")
}
}
oldAPI {
print("debug \($0)")
}
withCheckedContinuation은 비동기 컨텍스트를 일시 중단하고 continuation 객체를 제공한다.
콜백이 호출되면 continuation.resume(returning:)으로 중단된 작업을 재개하고
결과는 await 표현식의 값으로 반환된다.
func newAPI() async -> String {
await withCheckedContinuation { continuation in
oldAPI {
continuation.resume(returning: $0)
}
}
}
Task {
let result = await newAPI()
print("debug await \(result)")
}
주의할점이 있는데
CheckedContinuation 경우 continuation.resume()은 정확히 한 번만 호출되어야 한다. 여러 번 호출하면 런타임 오류가 발생 한다.
continuation이 resume되지 않으면 작업이 중단된 상태로 남아 메모리 누수와 교착 상태가 발생할 수 있다.
만약 oldAPI가 오류를 반환할 가능성이 있다면 withCheckedThrowingContinuation 를 통해 오류처리 또한 가능하다.
func oldAPI(completion: @escaping (Result<String, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if Bool.random() {
completion(.success("end"))
} else {
completion(.failure(NSError(domain: "APIError", code: 1, userInfo: nil)))
}
}
}
func newAPI() async throws -> String {
try await withCheckedThrowingContinuation { continuation in
oldAPI { result in
switch result {
case .success(let value):
continuation.resume(returning: value)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
Task {
do {
let result = try await newAPI()
print("debug await \(result)")
} catch {
print("debug \(error)")
}
}
resume이 정확히 한 번 호출되었는지 확인 한다.
continuation이 resume 없이 해제되면 경고 메시지를 기록한다.
SWIFT TASK CONTINUATION MISUSE: newAPI() leaked its continuation!
continuation은 중단된 비동기 흐름을 재개하는 것이기 때문이다.
continuation은 일시 중단된 작업의 실행 상태를 나타낸다.
continuation이 resume되면 저장된 실행 상태가 복원되고 함수 실행이 계속된다.
두 번 resume하면, 이미 진행 중이거나 완료된 함수의 실행 상태를 다시 복원하려는 시도가 발생한다.
이는 논리적 오류를 일으키는 것이다.
UnsafeContinuation 이러한 안전성 검사 resume이 여러 번 호출되는지, 비동기 컨텍스트가 정상적으로 처리되는지 검사하지 않기 때문에 런타임 체크 비용이 제거되므로 성능이 향상된다.
역시나 콜백 기반처럼 미리 애플이 만들어둔 델리게이트 기반 API들이 많다.
이걸 asnyc/await으로 전환하는 방법 또한 콜배 기반과 크게 다를게 없다.
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
let isFail: Bool
init(isFail: Bool) {
self.isFail = isFail
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
}
private var locationManager = CLLocationManager()
private var continuation: CheckedContinuation<String, Error>?
func requestLocation() async throws -> String {
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
if isFail {
continuation?.resume(throwing: error)
} else {
continuation?.resume(returning: "위치정보")
}
continuation = nil
}
}
let locationManager = LocationManager(isFail: false)
Task {
do {
let location = try await locationManager.requestLocation()
print("debug \(location)")
} catch {
print("debug \(error)")
}
}
옵셔널 continuation을 만들고,
requestLocation()을 호출하면,
self.continuation = continuation을 저장하여,
나중에 resume(returning:)을 호출할 때 사용한다.
위치 정보가 들어와 resume이 호출될때 까지 requestLocation 함수는 일시 정지 (suspend) 상태가 된다.
didFailWithError 델리게이트 메서드가 호출되면 resume 메서드를 실행하고,
requestLocation이 await에서 깨어나며 위치 데이터를 반환한다.
continuation = nil 을 통해 한 번 사용한 continuation을 초기화 한다.
콜백 기반 방식처럼 Checked, UnSafe continuation을 모두 사용할 수 있다.