Notice
Recent Posts
Recent Comments
Link
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
Archives
Today
Total
관리 메뉴

iOS 개발 노트

🪴Async/Await에 대해서 본문

Swift

🪴Async/Await에 대해서

Daeyun Kwon 2024. 6. 23. 22:01

Async/Await에 대해서

Async/Await은 비동기 프로그래밍을 위한 새로운 패러다임이다.
async 키워드를 사용하여 비동기 함수를 선언하고, await 키워드를 사용하여 해당 함수의 결과가 준비될 때까지 기다리는데, 이 과정에서 현재의 실행 흐름을 차단하지 않고 다른 작업을 계속 진행할 수 있다.

  • async 함수는 비동기적으로 실행되며, 결과를 반환하기 전에 완료될 필요가 있는 다른 비동기 작업을 await할 수 있다.
  • 비동기 함수 수행 중 문제가 발생하였을 때 error를 바로 리턴할 수 있도록 해준다

 

코루틴(Coroutine)

Coroutine은 함수가 동작하는 도중 특정 시점에 suspend(일시정지)할 수 있고, resume(다시 재개)할 수 있게 하는 비동기 함수 매커니즘이다.

  • await 키워드가 있는 곳이 일시정지 지점을 나타냄.
  • await 뒤에 있는 async 비동기 함수에 대해서 결과가 반환될 때까지 suspend가 작동된다.
  • 결과가 반환되면 resume!

💡 suspend → resume의 과정

  1. await 키워드를 만나면 suspension point로 지정하고 일시정지 (suspend)
  2. 스레드의 제어권을 시스템에게 넘겨줌
  3. 비동기 함수의 작업 실행이 완료되었을 때, 시스템이 다시 비동기 함수에게 스레드 제어권을 넘겨줌
  4. suspension point에서 작업 재개 (resume)

 

Async/Await 장점

  • 코드의 가독성 향상: 비동기 코드를 동기 코드와 유사하게 작성할 수 있어, 로직의 흐름을 쉽게 이해할 수 있습니다.
  • 콜백 지옥 해결: 중첩된 콜백 대신 선형적인 코드 흐름을 사용하여, 복잡한 비동기 로직을 간결하게 표현할 수 있습니다.
  • 에러 처리 용이: try/catch 구문을 사용하여 비동기 작업에서 발생하는 에러를 효과적으로 처리할 수 있습니다.
  • self 참조 사이클이 생길 우려 제거

Async/Await 단점

  • 성능 오버헤드: 비동기 작업이 많은 경우, Async/Await이 성능 면에서 약간의 오버헤드를 초래할 수 있습니다. 이는 작업을 스케줄링하고, 코루틴을 관리하는 데 필요한 추가적인 런타임 비용 때문일 수 있습니다.
  • 동기화 오버헤드: 동기화된 작업이 필요한 경우, Async/Await을 사용하면 추가적인 동기화 오버헤드가 발생할 수 있습니다. 이는 여러 비동기 작업 사이의 순서를 유지하거나 데이터를 공유할 때 발생할 수 있습니다.

 

Async/Await 사용해보기

함수명과 -> 기호 사이에 async 키워드를 써주면 해당 함수는 비동기로 실행된다.

func fetchData() async {
    print("비동기로 실행됩니다.")
}

func fetchData() async -> Data {
    print("비동기로 실행됩니다.")
}

 

async로 선언된 함수는 다른 async로 선언된 함수 내부 혹은 Task 클로저 내부(asynchronous context)에서만 호출할 수 있다. 그리고 async로 선언된 함수를 호출하기 위해서는 await 키워드와 함께 호출한다.

  • Task 클로저 내부에 비동기로 수행할 작업을 넣어주면 asynchronous context가 생성됨
  • Task 클로저 내부에 작업을 배치하는 것은 DispatchQueue.global.async에서 호출하는 것과 유사함
  • Task 클로저 내부에 있는 코드들은 처음부터 끝까지 순차적으로 실행됨
  • await 키워드가 붙은 곳은 비동기 작업이 완료될 때까지 일시정지 상태가 됨
func fetchData() async -> String {
    return "비동기로 실행됩니다."
}

func otherFunction() async {
    let string = await fetchData()
    print(string)
}

Task {
    await otherFunction() //비동기로 실행됩니다.
}

 

실행 중 문제가 발생할 가능성이 있어 Error를 던지고 싶다면 async 키워드 뒤에 throws 키워드를 써주면 된다.

func fetchData() async throws -> Data

호출할 때는 await 앞에 try 키워드를 사용해준다.

Task {
    do {
        let data = try await fetchData()
        // 데이터 처리 로직
    } catch {
        // 에러 처리 로직
    }
}

 

GCD와 다르게 비동기 작업 결과 값을 탈출 클로저를 이용해 전달할 필요가 없어서 코드 가독성이 좋다.

func addNumWithAsync() async -> Int {
    var result: Int = 0

    for i in 1...5 {
        result += i
    }

    return result
}

func callAsyncFunction() async {
    let result = await addNumWithAsync() //비동기 작업이 완료된 시점에 result로 값을 전달함
    print(result)
}

Task {
    await callAsyncFunction()    //15
}

 

각 독립적으로 동작하고 있는 Task들이 서로의 데이터를 공유하는 방법

Task {
    let pineapplePicking = Task {
        let pineapples = await harvestPineapples()
        return pineapples.randomElement()!
    }

    let pineapple = await pineapplePicking.value
}

공유한 파인애플이 값타입이라면 복사해서 넘겨주면 되므로 아무런 문제가 없다. 하지만 파인애플이 참조 타입이라면, 2개의 Task에서 파인애플 클래스에 동시에 write를 할 가능성이 있는 Data Race 상황이 발생한다는 문제가 있다.
참조 타입처럼 데이터를 공유하는 방식이 필요할 경우 Actor를 사용하면 된다.

 

Actor

한 번에 하나의 Task만 내부 상태를 조작하도록 허용함으로 동시 write로 인한 데이터 레이스를 피해주게 도와주는 것

  • class, struct, enum과 같은 object type임
  • 클래스와 마찬가지로 reference 타입임
  • 클래스와 다르게 상속은 지원하지 않고, 암시적으로 Sendable임
  • actor는 여러 스레드에서 동시에 실행 불가함

생긴 건 클래스와 비슷하고 참조 타입이다.

actor User {
    var name: String = ""
    var age: Int = 0
    let gender: String = "male"

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    func changeName(newName: String) {
        self.name = newName
    }

    func changeAge(newAge: Int) {
        self.age = newAge
    }
}

 

하지만 외부 사용법에 있어서는 클래스와 차이가 있다.

  • 상수는 바로 접근 가능하지만, 변수는 접근 시 await 키워드가 필요
  • actor 내부 변수값을 외부에서 직접 수정하는 것은 불가능함. 프로퍼티 변경은 actor 내부에서만 가능
  • actor 내부 메서드 호출 시 await 키워드 필요
Task {
    let user = User(name: "daeyun", age: 28)
    let gender = user.gender // 1. 상수는 변경 불가능하기 때문에 어느 스레드에서 접근해도 안전함. actor 외부에서도 바로 접근 가능
    let age = await user.age // 2. actor 외부에서 변수 접근시 await 필요
    await user.changeAge(newAge: 33) // 3. actor 외부에서 메서드 호출시 await 필요
    await user.age += 100 // 4. ❌ 컴파일 에러. actor 외부에서 actor 내부의 변수를 변경할 수 없음
}

💡 다른 Task 들이 actor에 접근해서 코드 실행하고 있으면 해당 actor에 대한 다른 코드는 실행되지 못하고 기다려야 한다. 그래서 await 키워드를 사용하는 것이다. await을 이용해 여러 Task에서 동시에 실행될 수 없도록 방지하는 셈이다.

참고

https://tech.devsisters.com/posts/crunchy-concurrency-swift/
https://chatgpt.com/
https://sujinnaljin.medium.com/swift-actor-뿌시기-249aee2b732d

'Swift' 카테고리의 다른 글

🪴열거형(Enumeration)  (0) 2024.06.12
🪴Swift는 Type-Safe한 언어!  (0) 2023.09.22
🪴변수명 표기법  (0) 2023.06.29