A crash course of async await (Swift Concurrency) - Shai Mishali - Swift Heroes 2022
private func loadImage(completion: @escaping (Result<UIImage, Error>) -> ()) {
// ... return result using completion
}
private func loadImage() async throws -> UIImage {
// ... return image
async
를 따른다고 선언한 뒤 해당 파라미터를 리턴throws
는 에러를 스로우할 수 있음을 보여줌private func testWithCompletion() {
loadImage { result in
switch result {
case .success(let image):
// handle image
break
case .failure(let error):
// handle error
break
}
}
}
private func testWithAsync() async {
do {
let image = try await loadImage()
} catch {
// error handling
}
}
async
를 따른다고 선언해야 async
, 비동기적으로 리턴하는 위의 함수를 사용 가능throws
를 통해 에러를 내보낼 수 있기 때문에 do catch
를 통해 에러 핸들링await
를 통해 비동기적 함수의 리턴 값이 나올 때까지 기다린 뒤 원하는 태스크를 실행 가능 private func testWithAsyncAndTask() {
Task {
do {
let image = try await loadImage()
} catch {
// error handling
}
}
}
async
를 통해 리턴하는 비동기 구문을 사용할 수 있는 또 다른 방법은 Task
블럭 내부에서 사용하는 것private func testWithAsyncAsSync() async {
do {
let firstImage = try await loadImage()
let secondImage = try await loadImage()
let thridImage = try await loadImage()
} catch {
// error handling
}
}
await
를 통해 리턴을 받을 때까지 기다림. 이후 resuming
이 가능한 구조Success
와 Failure
를 모두 제네릭하게 설정 가능thread exploision
을 방지Continuation
: 스레드 스위칭 없이 특정 작업을 정지, 재개하는 일이 경량화됨private func getAmazingItem() async -> UIImage? {
// ... return data
}
private func getFoo() async {
let tiem = await getAmazingItem()
}
getFoo()
함수는 getAmazingItem()
비동기 함수가 데이터를 리턴할 때까지 현재 스레드 상태를 정지한 뒤, 데이터가 리턴되면 그때 태스크를 재개getAmazingItem()
비동기 함수가 종료된다면, getFoo()
함수는 다시 작업을 재개하는 데, 이 싲머에서는 어느 종류의 스레드에서라도 리줌될 수 있음async throws
를 통해 발생 가능한 에러를 스로우하는 구문을 다른 태스크 / 비동기 구문에서 사용할 때 try catch
를 통해 손쉽게 에러 핸들링 가능 private func testWithCompletionCorrect(urlString: String, completion: @escaping ((Result<UIImage, Error>) -> Void)) {
guard let url = URL(string: urlString) else {
completion(.failure(URLError(.badURL)))
return
}
...
}
private func testWithCompletionWrong(urlString: String, completion: @escaping ((Result<UIImage, Error>) -> Void)) {
guard let url = URL(string: urlString) else {
return
}
...
}
return
문과 별도로 컴플리션 클로저 내부에 실패 상황을 작성하지 않아도 컴파일러가 통과시킴 private func testWithAsyncThrows(urlString: String) async throws -> UIImage? {
guard let url = URL(string: urlString) else {
throw URLError(.badURL)
}
...
}
async
구문을 통해 특정 데이터를 리턴하는 경우에는 throw
또는 리턴 데이터에 맞춰 값을 리턴해야 하기 때문에 코드를 잘못 작성하는 케이스가 감소struct Person {
let url: URL
var image: UIImage? {
get async throws {
let (data, _) = try await URLSession.shared.data(from: url)
return UIImage(data: data)
}
}
}
getter
를 비동기적으로 작성한 구문URLSession
의 async
를 따르는 data
를 비동기적으로 리턴private func testAsyncGetter() {
Task {
let person1 = Person(url: URL(string: "")!)
let imge = try await person1.image
}
}
Task
또는 async
를 따르는 구문 아래에서 await
작성async await
를 통한 비동기 사용 → 재귀 호출을 사용하지 않음 (만료가 되었다면 갱신을 하는 과정이 비동기적으로 이루어지는 데, 해당 과정이 await
를 통해 해당 태스크 전반을 대기시키기 때문에 그 과정이 보장이 된 시점에 해당 토큰을 곧바로 사용할 수 있기 때문)async
하게 리턴받는 과정 여러 개의 그룹을 순차적으로 실행하는 게 아니라, 병렬적으로 처리하고 싶을 때 사용 가능한 방법enter
, leave
에서 벗어나 코드 가독성 및 단축private func testWithDispatchGroup() {
let group = DispatchGroup()
let person1 = Person(url: URL(string: "")!)
let person2 = Person(url: URL(string: "")!)
let person3 = Person(url: URL(string: "")!)
var personImages: [UIImage] = []
group.enter()
person1.loadImage { result in
defer { group.leave() }
switch result {
case .success(let image): personImages.append(image)
case .failure(let error): break
}
}
group.enter()
person2.loadImage { result in
defer { group.leave() }
switch result {
case .success(let image): personImages.append(image)
case .failure(let error): break
}
}
group.enter()
person3.loadImage { result in
switch result {
case .success(let image): personImages.append(image)
case .failure(let error): break
}
}
group.notify(queue: DispatchQueue.main) {
handleImages(images: personImages)
}
}
notifiy
를 통해 특정 작업이 완료되었음을 감지 가능 → 해당 블럭에서의 images
배열은 각 비동기 블럭이 완료된 이후임을 보장할 수 있음func loadImage(completion: @escaping((Result<UIImage, Error>) -> Void)) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
completion(.failure(error))
} else if
let data = data,
let image = UIImage(data: data) {
completion(.success(image))
}
}
}
private func testWithAsyncLetBinding() async {
let person1 = Person(url: URL(string: "")!)
let person2 = Person(url: URL(string: "")!)
let person3 = Person(url: URL(string: "")!)
async let image1 = person1.image
async let image2 = person2.image
async let image3 = person3.image
do {
try await handleImages(images: [image1, image2, image3])
} catch {
// error handling
}
}
handleImages
함수가 실행되는 게 보장 (await
를 통해 해당 handleImages
로 들어오는 image1
... 들이 들어올 때까지 기다림)Cooperative Cancellation
메커니즘을 사용try Task.checkCancellation()
if Task.isCancelled {
// .. default value returned
}
var image: UIImage? {
get async throws {
try await withTaskCancellationHandler(operation: {
let (data, _) = try await URLSession.shared.data(from: url)
return UIImage(data: data)
}, onCancel: {
// ... handle someithing in this return Void clousre
})
}
}
async
리퀘스트를 보낸 뒤 리턴받은 모든 데이터를 한 번에 사용해야 하는 경우private func testWithForLoop(people: [Person]) async -> [UIImage?] {
var result = [UIImage?]()
for person in people {
do {
try await result.append(person.image)
} catch {
// error handling
}
}
return result
}
For-Loop
를 통해 파라미터로 들어온 각 변수 별로 await
를 통해 데이터가 리턴받을 때까지 서스펜션이 걸리기 때문에 병렬 처리가 아님async let binding
이 필요private func testWithAsyncLetBinding(people: [Person]) async -> [UIImage?] {
var result = [UIImage?]()
for person in people {
async let image = person.image
// ... cannot handle this image
}
return result
}
async ley
으로 바인딩되는 변수의 개수가 동적으로 변할 경우 해당 인자에 대한 직접적인 접근이 어려워진다는 한계private func testWithTaskGroups(people: [Person]) async -> [UIImage?] {
await withTaskGroup(of: UIImage?.self, body: { group in
for person in people {
group.addTask {
do {
let image = try await person.image
return image
} catch {
// error handling
return nil
}
}
}
var result = [UIImage?]()
for await image in group {
result.append(image)
}
return result
})
}
withTaskGroup
내부에서 어떤 종류의 데이터 타입을 취급하는 넣어준 뒤, 바디에서 그룹으로 실행할 같은 종류의 태스크를 태스크 그룹에 넣기of
뒤에 선언한 해당 데이터와 일치해야 함. 에러 핸들링 또한 do catch
문을 동일하게 적용for await
를 통해 해당 그룹 태스크가 컨커런트하게 리턴하는 image
를 그때마다 받아와서 결과 result
를 리턴 가능private func testWithTaskGroups(people: [Person]) async -> [UIImage?] {
await withTaskGroup(of: UIImage?.self, body: { group in
for person in people {
group.addTask(priority: person.isPrior ? .high : nil) {
do {
let image = try await person.image
return image
} catch {
// error handling
return nil
}
}
}
var result = [UIImage?]()
for await image in group {
result.append(image)
}
return result
})
}
private func testWithTaskGropus(people: [Person]) async -> [String: UIImage?] {
await withTaskGroup(of: (String, UIImage?).self, body: { group in
for person in people {
group.addTask {
do {
return try await (person.id, person.image)
} catch {
return (person.id, nil)
}
}
}
return await group.reduce(into: [String: UIImage?]()) { $0[$1.0] = $1.1 }
})
}
of
로 넘겨버리는 방법에 주목Task.detached
등을 통해 사용, 별도로 관리awaited asynchronously
하다는 것만 제외한다면for await in ...
를 적용한 것이 그 예시private func fetchLineSerailly(with largeCSV: URL) async {
Task {
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: largeCSV))
let lines = String(data: data, encoding: .utf8)?.components(separatedBy: "\n") ?? []
for line in lines {
// handle line...
}
}
}
for loop
를 통해 핸들링private func fetchLineAsyncSequences(with largeCSV: URL) async {
Task {
let (bytes, _) = try await URLSession.shared.bytes(for: URLRequest(url: largeCSV))
for try await line in bytes.lines {
// handle line...
}
}
}
bytes
라는 별도의 API를 통해 컨커런트하게 접근 가능private func handleLocalFile(with localFile: URL) async {
Task {
let handle = try FileHandle(forReadingFrom: localFile)
for try await line in handle.bytes.lines {
// handle line by line
}
}
}
actor DataSource {
let items = ["A", "B", "C", "D", "E"]
private var index = 0
func next() {
print(items[index])
if items.indices.contains(index + 1) {
index += 1
} else {
index = 0
}
}
}
private func testActor() {
let dataSource = DataSource()
Task.detached { await dataSource.next() }
Task.detached { await dataSource.next() }
Task.detached { await dataSource.next() }
}
next()
함수에 접근, 호출해야 할 경우 순차적 접근이 가능하도록 actor
클래스에서 내장 지원@MainActor class DataSource {
let items = ["A", "B", "C", "D", "E"]
private var index = 0
func next() {
print(items[index])
if items.indices.contains(index + 1) {
index += 1
} else {
index = 0
}
}
}
@MainActor private func testActor() {
let dataSource = DataSource()
Task.detached { await dataSource.next() }
Task.detached { await dataSource.next() }
Task.detached { await dataSource.next() }
}
@MainActor
프로토콜을 따름으로써 보장 가능actor DataSource {
let items = ["A", "B", "C", "D", "E"]
private var index = 0
func next() {
print(items[index])
if items.indices.contains(index + 1) {
index += 1
} else {
index = 0
}
}
nonisolated
func check() {
print("i'm not isolated to this thread")
}
}
private func testActor() {
let dataSource = DataSource()
Task.detached {await dataSource.next()}
dataSource.check()
}
nonisolated
를 통해 await
로 기다리지 않아도 곧바로 호출 가능하도록 설정Continuation
함수를 만들어 사용 가능private func testContinuation() {
withCheckedContinuation(<#T##body: (CheckedContinuation<T, Never>) -> Void##(CheckedContinuation<T, Never>) -> Void#>)
withCheckedThrowingContinuation(<#T##body: (CheckedContinuation<T, Error>) -> Void##(CheckedContinuation<T, Error>) -> Void#>)
withUnsafeContinuation(<#T##fn: (UnsafeContinuation<T, Never>) -> Void##(UnsafeContinuation<T, Never>) -> Void#>)
withUnsafeThrowingContinuation(<#T##fn: (UnsafeContinuation<T, Error>) -> Void##(UnsafeContinuation<T, Error>) -> Void#>)
}
private func testAsyncWithCompletion(completion: @escaping((Result<UIImage, Error>) -> ())) { }
private func testAsyncContinuationWithCompletion() async throws -> UIImage {
try await withCheckedThrowingContinuation({ continuation in
testAsyncWithCompletion { result in
continuation.resume(with: result)
}
})
}
async await
를 따르도록 리팩터링 가능struct Item {
let id = UUID()
}
enum ItemChange {
case change(Item)
case finished
}
func checkItems(change: @escaping((ItemChange) -> ())) {
// handle async task
}
func checkItems() -> AsyncStream<Item> {
AsyncStream<Item> { continuation in
checkItems { change in
switch change {
case .change(let item): continuation.yield(item)
case .finished: continuation.finish()
}
}
}
}
private func testCheckItems() async {
Task {
for await item in checkItems() {
print("Current item: \(item)")
}
}
}
AsyncStream
을 통해 컴플리션을 감싸서 사용 가능.task
모디파이어를 통해 iOS 15 부터 사용 가능Task
를 통해 직접적으로 iOS 13부터 사용 가능했음values
프로퍼티를 통해 AsyncSequence
와 동일한 작업 수행 가능private func testCombineStyle(with numbers: AnyPublisher<Int, Error>) {
numbers
.map({$0 * 2})
.prefix(3)
.dropFirst()
.sink { _ in
// handle completion
} receiveValue: { number in
// handle number
}
}
private func testCombineAndAsync(with numbers: AnyPublisher<Int, Error>) async {
let publisher = numbers
.map({$0 * 2})
.prefix(3)
.dropFirst()
Task {
for try await number in publisher.values {
// handle number
}
// handle completion
}
}
values
가 정확히 AsyncSequences
와 유사하기 때문에 sink
단에서 실제로 내려오는 데이터를 for (try) await
로 다룰 수 있음AsyncSequence
를 위해 애플이 기본적으로 제공하는 오퍼레이터가 현재 컴바인 오퍼레이터를 대체할 수 있다는 게 유력함.