Deep Dive to Swift Concurrency - async let

TobyToby
3 min read

async let 에 대해서

보통 Swift Concurrency에 대해서 사용하고자 하면 다음과 같이 사용할 것이다.

func fetchOneThumbnail(withID id: String) async throws -> UIImage {
    let imageReq = imageRequest(for: id)
    let metadataReq = metadataRequest(for: id)
    let (data, _) = try await URLSession.shared.data(for: imageReq)
    let (metadata, _) = try await URLSession.shared.data(for: metadataReq)
    guard let size = parseSize(from: metadata),
          let image = UIImage(data: data)?.byPreparingThumbnail(ofSize: size)
    else {
        throw ThumbnailFailedError()
    }
    return image
}

분명 이 함수는 concurrency 한가? 그렇다.

하지만 정.말.로 작업이 concurreny 한지 살펴보자

아마 모든 스레드가 바뻐서 스레드 제어권을 바로 받을 수 없다면

await으로 인해서 스레드 제어권을 받기를 기다리고 있을 것이다.

더군다나 data와 metadata는 서로 의존적이지 않다.

metadata를 얻기 위해 data의 await을 기다릴 필요는 없다.

그래서 나온 것이 async let 이라는 개념이다.

func fetchOneThumbnail(withID id: String) async throws -> UIImage {
    let imageReq = imageRequest(for: id)
    let metadataReq = metadataRequest(for: id)
    async let (data, _) = URLSession.shared.data(for: imageReq)
    async let (metadata, _) = URLSession.shared.data(for: metadataReq)
    guard let size = parseSize(from: try await metadata),
          let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size)
    else {
        throw ThumbnailFailedError()
    }
    return image
}

async let 을 사용하면 자식 task를 만들어서 따로 실행시킨다.

그리고 기존(부모)task는 마저 fetchOneThumbnail에 있는 코드를 작동시킨다.

그리고 자식 task의 결과 값을 await을 통해서 받는다.

이 과정을 통해 이미지 가져오는 것을 조금 더 concurrency하게 갖고 올 수 있다.

이걸 이 때 자식 task 만드는 task를 부모 task라 한다.

async let에서 throw 처리 방법

func fetchOneThumbnail(withID id: String) async throws -> UIImage {
    let imageReq = imageRequest(for: id)
    let metadataReq = metadataRequest(for: id)
    async let (data, _) = URLSession.shared.data(for: imageReq) // 1️⃣
    async let (metadata, _) = URLSession.shared.data(for: metadataReq) // 2️⃣
    guard let size = parseSize(from: try await metadata),
          let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size)
    else {
        throw ThumbnailFailedError()
    }
    return image
}

만약 자식 task에서 에러가 발생하면 어떻게 될까?

코드로 알 수 있는 async let에 사용된 변수는 try await로 받는 점에서

자식 -> 부모 순으로 error를 던지는 것을 알 수 있다.

그럼 2️⃣에서 Error가 발생하면 1️⃣ 의 자식 task는 어떻게 되는 것일까?

이는 예시를 만들어서 보자

enum CustomError: Error {
    case myError
}

Task {
    async let num = AsyncTestFunction()
    sleep(3)
    throw CustomError.myError
    print("Task result \(await num)")
}


func AsyncTestFunction() async -> Int {
    for i in 1...10 {
        print(i)
        sleep(1)
    }
    return 100
}
// 출력 값: 1 2 3 4 5 6 7 8 9 10

일반적으로 생각 할 때 부모 task가 Error를 던지면 자식 task도 종료될 것 같지만 아니다.

부모Task는 자식Task에게 너 이제 필요 없어! 너 취소야 를 알려준다.

그렇기에 필요없는 리소스를 줄이기 위해서는 자식Task가 나 끝난거야? 를 체크하고 종료해야 한다.

이 때 사용되는 함수가 2가지가 있다.

  • Task.checkCancellation()

  • Task.isCancelled

Task.checkCancellation() 는 Error를 던진다. 그렇기에 throw 있는 구문이 적당할 것이다.

Task.isCancelled는 Bool 값이다. 취소 되었으면 true 값이다.

그러니 위 예시 코드를 아래와 같이 바꾸면 필요 없는 리소스를 줄일 수 있다.

enum CustomError: Error {
    case myError
}

Task {
    async let num = AsyncTestFunction()
    sleep(3)
    throw CustomError.myError
    print("Task result \(await try num)")
}


func AsyncTestFunction() async -> Int {
    for i in 1...10 {
        print(i)
        sleep(1)
        if Task.isCancelled { return -1 } // ✨
    }
    return 10
}
// 출력 값: 1 2 3

정리

  • 부모Task는 자식Task를 종료시키지 않고 종료됨을 Mark 해준다.

  • 자식Task가 필요 없을 경우가 있다면 적절하게 관리가 필요하다.

잉? 난 자식Task가 종료되어도 계속해서 작업하고 싶은데? 그럴 때는 어떻게 하지?

그건 2편으로 찾아 뵙겠습니다.

0
Subscribe to my newsletter

Read articles from Toby directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Toby
Toby

안녕하세요🙇🏻‍♂️ 세상을 더 편리하게 바꾸고 싶은 iOS 개발자 최인호입니다. Hello 👋 I'm Inho Choi, an iOS developer who wants to change the world more conveniently. 대학교 졸업 Apple Developer Academy @ POSTECH 1기 KWDC Main Organizer AsyncSwift Organizer