
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를 위해 애플이 기본적으로 제공하는 오퍼레이터가 현재 컴바인 오퍼레이터를 대체할 수 있다는 게 유력함.