Swift 5.5 [Swift 5.5] async await

[Swift 5.5] async await

Javascript, kotlin, google-promises 등 그 동안 다른 언어에서 제공되던 기능이 Swift 5.5 에 강력하게 들어왔다.
비동기 프로그래밍은 그동안 delegate 패턴이나, closur 를 통해 해왔다면 앞으로는 async, await 를 통한 동기 프로그래밍을 작성하는 것 처럼 비동기 프로그래밍을 할 수 있게 되었다.

컴파일 타임에 concurrency 에 대한 문제 파악이 가능하다고 한다.

WWDC 21 - Meet AsyncSequence
WWDC 21 - Explore structed concurrency in Swift

async

비동기 함수 정의

func listPhotos(inGallery name: String) async -> [String] {
    let result = // ... some asynchronous networking code ...
    return result
}

await

비동기 함수의 실행을 동기시키는 명령
스레드에서 해당 비동기 함수의 실행이 끝날 때까지 대기(yielding the thread)

let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)
  • listPhotos(inGallery: “Summer Vacation”) 가 완료 될때까지 대기했다가 다시 실행된다.

비동기 시퀀스

let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
    print(line)
}
  • 시퀀스도 가능하다.
  • “for- await- in” 루프를 통해 접근한다.
  • AsyncSequence 프로토콜도 지원하는데, API 의 progress 처럼 지속적인 값을 가져올 시 필요한 듯 하다.(확실 치 않음)

병렬 비동기 함수 호출

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])

let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
  • 순차 실행 - Rx 의 concat 이라고 이해 했다.
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
  • 병렬 실행(async-let)

Task

비동기 작업의 단위, 생성 시 우선순위를 줄 수 있다.

let newPhoto = // ... some photo data ...
let handle = Task {
    return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value

Task

Task Groups

await withTaskGroup(of: Data.self) { taskGroup in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
        taskGroup.async { await downloadPhoto(named: name) }
    }
}

TaskGroup을 통한 병렬 실행

Task Cancellation

Task.checkCancellation() throw == CancellationError// 취소 확인
Task.isCancelled // 취소 확인
Task.cancel() // 취소

코드 작성

한번 작성해 보자! 가장 쉽고 적당한(?) 이미지 다운로드를 해보자.
Image

class ViewController: UIViewController {
    @IBOutlet weak var imageView: CustomImageView!
    let viewModel = ImageViewModel()

    // ViewController 에서는 UIImageView 를 가지고 viewDidLoad 가 되면 이미지 다운로드 시작을 하고 싶었다.
    override func viewDidLoad() {
        super.viewDidLoad()

        Task {
            do {
                let image = try await viewModel.fetchImage(with: "https://images.unsplash.com/photo-1629491011862-af5720ac882a?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80")

                imageView.image = image
            } catch {

            }
        }
    }
}
//  이미지 다운로드 시 에러를 정의해 주었다.
enum ImageDownloadError: Error {
    case invalidURLString
    case invalidImage
    case invalidResponse
}
class ImageViewModel {
    // 실제 이미지 다운로드 처리를 하는 곳이다.
    func fetchImage(with urlString: String) async throws -> UIImage {
        guard let url = URL(string: urlString) else {
            throw ImageDownloadError.invalidURLString
        }

        // 1. await 사용
        let (data, response) = try await URLSession.shared.data(from: url, delegate: nil)

        guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
            throw ImageDownloadError.invalidResponse
        }

        // 2. await 사용
        // 여기에 왜 await 가 들어가는지 잘 모르겠다. 안넣으면 에러가 나서 일단 넣었다. 추후 알아봐야 겠다.
        guard let image = await UIImage(data: data, scale: UIScreen.main.scale) else {
            throw ImageDownloadError.invalidImage
        }

        // 기존 코드와 다른 점은 return image 로 데이터를 내보낸다.
        return image
    }
}

Image

  • 실행 화면이다. 잘된다!

디버깅

DispatchQueue.main.async {

}

기존 방식대로라면 위의 코드를 넣어 메인스레드에서 image 를 추가해 주어야 한다. 그런데 작성한 코드가 메인쓰레드로 변경해주는 코드가 없다.

Image Image Image 쓰레드 변경 Image Image

Image imageView.image = image 가 메인스레드에서 동작한는 것은 viewDidLoad() 에서 다운로드 완료를 await 하고 완료 되었을대 실행 해준다. 디버깅이 어려운 Rx 에 비해 코드가 간결하고 쉽다라는 생각이 들었다.

참고

Swift.org - Concurrency

댓글남기기