Skip to content

async / await #4

@camosss

Description

@camosss

async / await

  • async: 비동기 함수, 호출 시 await 필요
  • async throws: 에러 가능성 있는 비동기 함수, 호출 시 try await 필요
구분 async async throws
에러 가능성 없음 있음 (throw)
호출 시 await try await
에러 처리 필요 없음 do-catch / try? / try!

동기 함수에서는 직접 await 사용 불가 -> Task {} 안에서 호출해야 함


기존 방식 vs Concurrency 방식

기존 비동기 함수 정의 방법

  • 오래 걸려서 얻는 결과값을 반드시 콜백 클로저로만 결과 전달 가능
func getImage(callback: @escaping (UIImage?) -> Void) {
    DispatchQueue.global().async {
        sleep(5)
        callback(UIImage(systemName: "star"))
    }
}

Concurrency 비동기 함수 정의 방법

  • 함수로 정의 → 결과값 직접 반환 가능
func getImage() async -> UIImage? {
    try? await Task.sleep(for: .seconds(2))
    return UIImage(systemName: "star")
}

Task {
    let image = await getImage()
}

sleep vs Task.sleep

  • sleep
    • 스레드를 점유하고, 아예 일정시간동안 해당 스레드를 멈추고 동작시키지 않는 함수 (blocking) → 비효율적

    • 메인 스레드에서 사용 시 UI 멈춤

func blockingSleep() {
    print("blocking 시작")

    // 2초 동안 스레드를 완전히 점유 ->  메인 스레드가 멈춰서 그동안 아무 것도 못 함
    sleep(2) 
    print("blocking 끝")
}

blocking 시작
(2초 멈춤)
blocking 끝
  • Task.sleep
    • 비동기 함수, Task만 일시 중단 후 재개 (non-blocking) → 효율적

    • 시간이 종료되기 전에 작업이 취소되면, CancellationError를 던짐

func nonBlockingSleep() {
    Task {
        print("non-blocking 시작")

        // 스레드 점유하지 않고 다른 Task코드가 계속 실행됨
        try await Task.sleep(for: .seconds(2))
        print("non-blocking 끝")
    }
}

non-blocking 시작
(2초 후 재개)
non-blocking 끝

GCD vs Swift Concurrency

GCD 비동기

  • 다른 스레드로 작업을 던짐 → 끝날 때 콜백으로 결과 전달
  • 흐름 제어가 콜백 클로저에 의존
func method(closure: @escaping (String) -> Void) {
    closure("결과")
}

method { result in
    print(result)
}

Concurrency 비동기 함수 (async/await)

  • 비동기 함수 자체가 값 반환 가능 → 콜백 불필요
  • 실행 중간에 멈췄다가 재개 가능 (non-blocking)
func asyncMethod() async -> String {
    let result = await otherMethod()
    return result
}

Task {
    let result = await asyncMethod()
    print(result)
}
  • 명시적으로 @MainActor 지정하지 않으면 → 런타임이 적절한 실행자(executor) 선택 (보통 백그라운드)
@MainActor
func updateUI() {
    label.text = "업데이트 완료"
}

// Task 생성 시 메인 액터 지정
Task { @MainActor in
    // 이 블록 안 코드는 메인 스레드에서 실행
    print("main:", Thread.isMainThread) // true
}

// 특정 구문만 메인 액터로 hop (실행자(executor) 전환)
// - `async` 함수는 실행 중간에 다른 실행자(executor) 로 넘어갈 수 있음
// - `DispatchQueue.main.async {}`랑 비슷한 개념이지만, Swift Concurrency는 컴파일러가 보장
Task {
    let data = await fetchData() // 백그라운드에서 실행될 수 있음
    
    await MainActor.run {
        // 여기서만 메인 스레드 보장
        imageView.image = UIImage(data: data)
    }
}

에러 처리

  • async throws

    • async throws: 비동기 함수도 에러를 던질 수 있음 → 호출 시 try await

    • 에러는 필요 시 do-catch로 처리

    • 함수가 동기/비동기적이든 결과가 에러가 발생할 수 있다는 것이 더 포괄적인 개념

  • Task도 에러를 담을 수 있음

    • Task 안에서는 do-catch로 에러처리를 하지 않아도, Task 내부로 에러를 던질 수 있음

    • Task<Success, Failure: Error>: Task가 에러를 가지고 있음

    • 필요한 경우 Task 안에서 에러 처리

Task.yield() 명시적인 중단 지점 만들기

  • 협력적 스케줄링(cooperative scheduling)을 도와주는 용도 -> 긴 루프 안에서 적절히 써주면 좋음
    • 중단 지점이 없는 긴 실행 작업의 중간에서 자발적으로 자신을 중단시켜,
    • 다른 작업이 잠시 실행될 수 있도록 한 후에, 이 작업으로 실행이 돌아오게 할 수 있음
func getImagesArray() async -> [UIImage?] {
    let image1 = await getImage()
  
    // 다른 작업에 CPU 양보
    await Task.yield()  
    let image2 = await getImage()
    let image3 = await getImage()

    return [image1, image2, image3]
}

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions