이전글 - Concurrency(4) Custom Asynchronous Sequences With AsyncStream
Modern Concurrency in Swift 를 읽고 간단 정리
이전 글에서 NotificationCenter 처럼 기존 API에 async/await을 적용하는 것을 살펴보았다.
이번 글에서도 이어서 기존 코드들에 Swift Concurrency를 활용하는 방안에 대해 소개 !
Introducing Continuations
이전 애플의 주요한 비동기 방식
- completion callbacks
- delegate
--> continuation을 활용하여 Swift 동시성 모델로 전환할 수 있다 !
Continuation
- 특정 시점에 프로그램의 상태를 추적하는 객체
- Swift 동시성 모델은 비동기 작업의 각 단위에 대해 전체 쓰레드를 생성하는 대신 Continuation을 할당
- 동시성 모델은 쓰레드 생성을 CPU 코어 개수만큼으로 제한
- 쓰레드 간의 전환 대신 Continuation 간의 전환이 이뤄져 더 효율적
await 호출과 실행 중단
- await 키워드를 호출하면 현재 코드의 실행을 일시 중단 (suspend)
- 이때 함수의 상태들 (변수, 스택, 실행 흐름 등)은 heap 영역에 저장
Continuation의 생성과 사용
- 코드가 await 호출로 중단될 때, Continuation이 생성
- 비동기 작업이 완료되면, 이 Continuation을 이용해 중단된 상태를 복원하고 작업을 재개 (resume)
--> Continuation을 직접 만들어 callback, delegate 기반의 기존 코드를 비동기 모델에 맞게 확장할 수 있다 !
Creating Continuations Manually
- 중단될 실행을 재개하거나 오류를 던질 수 있는 매커니즘
- 런타임 체크를 제공
- 성능이 중요하고 안전성 검사가 필요 없을 때 CheckedContinuation의 대안
생성
- A continuation is an opaque representation of program state.
- Contination은 프로그램의 상태를 나타내지만, 그 내부 구조나 상태가 직접적으로 드러나지는 않는다.
- "Opaque Representation" 이라는 것은 Continuation이 내부적으로 프로그램의 중단된 상태를 관리하지만, 이 상태에 대한 직접적인 접근은 제한된다는 의미로 생각됨
- withCheckedContinuation(isolation:function:_:)
- withChecekdThrowingContinuation(isoltation:function:_:)
- withUnsafeContinuation(isolation:_:)
- withUnsafeThrowingContinuation(isolation:_:)
- Unsafe와 다르게 Checked에는 function 파라미터가 있는데, 이는 런타임 검사 시에 Continuation의 출처를 쉽게 제공하기 위한 문자열이다.
resume
- Continuation은 반드시 한 번 resume을 호출해야 함
- 재개하지 않으면 해당 작업이 무기한으로 중단된 상태로 남음
- ChekcedContinuation은 이러한 조건이 위반되는지 런타임 검사를 수행
- UnsafeContinuation은 조건 검사를 런타임에 강제하지 않음
- 이는 Swift 작업을 event loop, delegate method, callback 등 비동기 스케줄링 메커니즘과 연결하는데 있어 낮은 오버헤드를 목표로 하기 때문
[ resume을 0번 호출하는 경우 ]
- Checked는 런타임에서 아래와 같은 콘솔 메세지 노출
- Unsafe는 묵묵부답
[resume을 2회 호출하는 경우]
- Checked는 런타임 에러 + 친절한 이유 설명
- Unsafe는 불친절한 런타임 에러
Wrapping the Deleagte Pattern
이 책에서는 CoreLocation에서 위치를 찾는 동작을 async/await 코드로 wrapping 하는 방법을 소개한다.
delegate 패턴에서 현재 위치 값을 받아오기 위해 다음의 동작들이 필요하다.
- CLLocationManager를 이용하여 위치 권한 허용 요청
- 위치 권한이 허용되어 있다면 위치를 manager에서 확인하도록 요청
- CLLocationManagerDelegate에서 성공, 실패 여부를 받아서 처리
CheckedContinuation을 이용하여 변경할 수 있는데 주의할 점은 resume이 한 번 호출되지 않는 상황들(0회 또는 2회 이상)을 방어하는 것이다.
@MainActor
class BlabberModel: ObservableObject {
private let manager = CLLocationManager()
private var delegate: ChatLocationDelegate?
...
/// Shares the current user's address in chat.
func shareLocation() async throws {
let location: CLLocation = try await withCheckedThrowingContinuation { [weak self] continuation in
guard let self else {
continuation.resume(throwing: "self is not known referenced")
return
}
self.delegate = ChatLocationDelegate(manager: self.manager, continuation: continuation)
if self.manager.authorizationStatus == .authorizedWhenInUse {
self.manager.startUpdatingLocation()
}
}
print(location.description)
self.manager.stopUpdatingLocation()
self.delegate = nil
}
}
import Foundation
import CoreLocation
class ChatLocationDelegate: NSObject, CLLocationManagerDelegate {
typealias LocationContinuation = CheckedContinuation<CLLocation, Error>
private var continuation: LocationContinuation?
init(manager: CLLocationManager, continuation: LocationContinuation) {
self.continuation = continuation
super.init()
manager.delegate = self
manager.requestWhenInUseAuthorization()
}
deinit {
self.continuation?.resume(throwing: CancellationError())
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .notDetermined:
break
case .authorizedAlways, .authorizedWhenInUse:
manager.startUpdatingLocation()
default:
self.continuation?.resume(throwing: "The app isn't authorized to use location Data")
self.continuation = nil
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
self.continuation?.resume(returning: location)
self.continuation = nil
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
self.continuation?.resume(throwing: error)
self.continuation = nil
}
}
- 위치 권한을 허용하지 않았을 경우 LocationManager는 초기 1회에 에러를 던지고 그 이후에는 던지지 않는다.
- 이 때문에 deinit에서 continuation resume 처리를 추가로 해주고 있다.
- CancellationError는 작업이 취소되었을 때 알려주는 스탠다드한 방법이라고 책에서 소개
Wrapping Callback APIs With Continuation
이번에는 completion callback을 async/await 코드로 wrapping하는 방법을 소개한다.
CLLoaction을 String으로 변환해주는 Encoder가 아래와 같이 있다.
enum AddressEncoder {
/// Converts the given location into the nearest address, calls `completion` when finished.
///
/// - Note: This method is "simulating" an old-style callback API that the reader can wrap
/// as an async code while working through the book.
static func addressFor(location: CLLocation, completion: @escaping (String?, Error?) -> Void) {
let geocoder = CLGeocoder()
Task {
do {
guard
let placemark = try await geocoder.reverseGeocodeLocation(location).first,
let address = placemark.postalAddress else {
completion(nil, "No addresses found")
return
}
completion(CNPostalAddressFormatter.string(from: address, style: .mailingAddress), nil)
} catch {
completion(nil, error)
}
}
}
}
사용하는 쪽에서 Continuation으로 묶어주면 끝 !
let address: String = try await withCheckedThrowingContinuation { continuation in
AddressEncoder.addressFor(location: location) { address, error in
switch (address, error) {
case (nil, let error?):
continuation.resume(throwing: error)
case (let address?, nil):
continuation.resume(returning: address)
case (nil, nil):
continuation.resume(throwing: "Address encoding Failed")
case let (address?, _):
continuation.resume(returning: address)
}
}
'iOS > Concurrency' 카테고리의 다른 글
Concurrency (7) Concurrent Code With TaskGroup (0) | 2024.09.15 |
---|---|
Concurrency (6) Testing Asynchronous Code (1) | 2024.09.08 |
Concurrency (4) Custom Asynchronous Sequences With AsyncStream (0) | 2024.08.24 |
Concurrency (3) AsyncSequence & Intermediate Task (0) | 2024.08.15 |
Concurrency (2) Getting started With async/await (0) | 2024.08.15 |