study record
[Swift] Concurrency 사용하기 본문
Swift Concurrency - Async, Await
기존에 비동기 처리 방식은 DispatchQueue나 completionHandler를 사용하여 처리했지만 Swift 5.5에서 구현된 Async Await은 더욱 편하게 비동기 처리할 수 있는 문법이다.
// DispatchQueue 사용한 비동기 처리
DispatchQueue.global.async {
}
// completionHandler를 사용한 비동기 처리
let task = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
}).resume()
- 비동기 처리란 오래 걸리는 작업이 있을 때, 그 작업이 끝나는 것을 기다리지 않고 수행하도록 일을 처리하는 것이다.
- 대표적인 비동기 처리 방법에는 RxSwift, Combine 등이 있다.
Async, Await 등장 배경 - 콜백지옥
completionHandler 콜백 문법은 콜백의 연속이 되어버리는 코드를 만든다.
func processImageData1(completionBlock: (_ result: Image) -> Void) {
loadWebResource("dataprofile.txt") { dataResource in
loadWebResource("imagedata.dat") { imageResource in
decodeImage(dataResource, imageResource) { imageTmp in
dewarpAndCleanupImage(imageTmp) { imageResult in
completionBlock(imageResult)
}
}
}
}
}
processImageData1 { image in
display(image)
}
에러 핸들링 또한 복잡하게 작성된다.
func processImageData2c(completionBlock: (Result<Image, Error>) -> Void) {
loadWebResource("dataprofile.txt") { dataResourceResult in
switch dataResourceResult {
case .success(let dataResource):
loadWebResource("imagedata.dat") { imageResourceResult in
switch imageResourceResult {
case .success(let imageResource):
decodeImage(dataResource, imageResource) { imageTmpResult in
switch imageTmpResult {
case .success(let imageTmp):
dewarpAndCleanupImage(imageTmp) { imageResult in
completionBlock(imageResult)
}
case .failure(let error):
completionBlock(.failure(error))
}
}
case .failure(let error):
completionBlock(.failure(error))
}
}
case .failure(let error):
completionBlock(.failure(error))
}
}
}
processImageData2c { result in
switch result {
case .success(let image):
display(image)
case .failure(let error):
display("No image today", error)
}
}
이러한 문제를 async, await을 통해 간결하게 코드를 작성할 수 있다.
다음과 같이 동기적으로 일어나는 것처럼 코드를 작성할 수 있어서 이해를 돕는다.
func loadWebResource(_ path: String) async throws -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image
func dewarpAndCleanupImage(_ i : Image) async throws -> Image
func processImageData() async throws -> Image {
let dataResource = try await loadWebResource("dataprofile.txt")
let imageResource = try await loadWebResource("imagedata.dat")
let imageTmp = try await decodeImage(dataResource, imageResource)
let imageResult = try await dewarpAndCleanupImage(imageTmp)
return imageResult
}
Async Await을 처리하는 원리 (스레드 관리)
sync에서의 스레드 관리
- 호출 : A함수에서 B함수를 호출하면 A함수가 실행되던 스레드의 제어권을 B에게 전달
- 진행 : B함수가 끝날 때까지 해당 스레드는 점유되어 다른 일을 수행하지 않는다.
- 종료 : B함수가 종료되면 A함수에게 다시 스레드 제어권을 반납한다.
async에서의 스레드 관리
- 호출 : A함수에서 B함수를 호출하면 A함수가 실행되던 스레드의 제어권을 B에게 전달
- 진행 : B함수는 async이기 때문에 스레드의 제어권을 포기하는 suspend가 가능하다.
- suspend : 스레드에 대한 제어권은 system으로 가고 시스템은 스레드를 사용해 다른 작업을 수행한다.
- resume : 일시 중단된 B함수를 다시 실행한다.
- 종료 : B함수가 종료되면 A함수에게 스레드 제어권을 반납한다.
await async 사용법
- 메서드 시그니처에 async를 붙여서 비동기 작업임을 알리는 키워드를 추가
- 메서드 내부 구현에서 비동기가 예상되는 곳에 await키워드를 추가
- 해당 메서드를 호출하는 쪽에서는 Task block에서 호출하고, await 키워드를 붙여서 호출
async 함수를 호출하기 위해서는 await 키워드가 필요합니다.
@objc private func didTapButton() {
guard let url = URL(string: "https://reqres.in/api/users?page=2") else { return }
Task {
guard
let imageURL = try? await self.requestImageURL(requestURL: url),
let url = URL(string: imageURL),
let data = try? Data(contentsOf: url)
else { return }
print(Thread.isMainThread) // true
self.imageView.image = UIImage(data: data)
}
}
func requestImageURL(requestURL: URL) async throws -> String {
print(Thread.isMainThread) // false
let (data, _) = try await URLSession.shared.data(from: requestURL)
return try JSONDecoder().decode(MyModel.self, from: data).data.first?.avatar ?? ""
}
# 개념
✔️ Asynchronous Function - 일시중단될 수 있으며 그동안 다른 비동기 함수가 해당 스레드에서 실행될 수 있음.
✔️ Asynchronous Sequence - collection의 element를 하나씩 기다릴 수 있음. with for-await-loop
✔️ Asynchronous Function in parallel - 순차적 진행이 아니라 아니라 병렬로 작업을 진행할 수 있음. with async-let
✔️ Tasks and Task Groups - 비동기 코드의 우선순위, 취소 처리에 대해 더 많이 컨트롤 할 수 있음.
(TaskGroup에 Task를 추가하며 사용하는 경우를 structured concurrency, 단독 Task를 사용하는 경우를 unstructured concurrency 라고 부름)
✔️ Actor - mutable state에 안전하게 접근할 수 있음.
# 키워드
✔️ async - function이 asynchronous 하다는 것을 나타내기 위해 작성
✔️ await - possible suspension point 를 나타내기 위해 작성
suspension point ?
suspension point(잠재적인 일시 중단 지점)라는 용어가 자주 등장하는데 잘 이해가 되지 않았었다.
->
func process() async {
let data = await self.getData()
let contents = await self.decode(data: data)
print(contents)
}
다음의 함수들과 같이 함수를 호출하는 동안 작업이 일시 중단되어야 하기 때문이다.
getData()를 통해 데이터를 가져오고 있는데 작업을 계속 진행시키면 decode할 데이터가 아직 준비되어 있지 않은 상태에서 decode(data)가 실행되게 되어 흐름이 이상해질 것임을 이해할 수 있다.
await 키워드를 만나면 그곳에서 suspend(일시중지)될 수 있다. suspend(일시 중단)은 해당 스레드가 다른 동작을 하게끔 제어권을 놓아준다는 뜻이다. 스레드를 block한다는 게 아니고 제어권을 넘긴다. 이렇게 되면 시스템에 스레드에 대한 제어권을 가져서 다른 작업을 수행하고 시스템이 우선 순위를 판단해 가며 여러 작업을 실행하다가, 일시 중단되었던 비동기 함수를 다시 실행하는 것이 중요하다고 판단될 때 제어권을 받아 재개(resume)할 수 있는 것이다. 이때의 스레드는 다른 스레드일 수 있다.
이렇게 정지된 상태에서 다른 작업이 수행될 수 있으므로 await 키워드로 비동기 호출을 표시한다. 스레드 양보라고도 불린다.
또한 suspension point에서 유지되는 모든 정보는 힙에 저장되어 나중에 실행을 계속하는데 사용할 수 있다.
Task의 역할
Task는 sync와 async 같의 다리 역할을 한다. 명시적으로 비동기화 컨텍스트를 생성하여 동기화 컨텍스트에서도 비동기를 호출할 수 있다.
Task는 Swift가 코드를 병렬로 실행하는 기본 메커니즘이다.
참고
- https://ios-development.tistory.com/958
- https://zeddios.tistory.com/1230
- https://sujinnaljin.medium.com/swift-async-await-concurrency-bd7bcf34e26f
'Swift > 스위프트 정리' 카테고리의 다른 글
[Swift] Actor란? - 1 (0) | 2022.12.18 |
---|---|
[Swift] SwiftUI란? (0) | 2022.11.23 |
[Swift] Concurrency - 2 (1) | 2022.11.12 |
[Swift] Concurrency - 1 (0) | 2022.10.30 |
[Swift] 클로저, 탈출 클로저와 메모리 (0) | 2022.05.23 |