본문 바로가기

iOS/Concurrency

Concurrency (2) Getting started With async/await

이전글 - Concurrency(1) Why Modern Concurrency?

 

Modern Concurrency in Swift 를 읽고 간단 정리


 

Pre-async/await Asynchrony

func fetchStatus(completion: @escaping (ServerStatus) -> Void) {
    URLSession.shared.dataTask(
        with: URL(string: "https://amazingserver.com/status")!
    ) { data, response, error in
        // Decoding, error handling etc
        completion(ServerStatus(data))
    }.resume()
}

fetchStatus { [weak viewModel] status in
    guard let viewModel else { return }
    viewModel.serverStatus = status
}

 

많은 문제

- 컴파일러가 fetchStatus 내에서 얼마나 많은 컴플리션 핸들러를 호출할지 알 수 없음 

- 약한 참조를 사용하여 메모리릭 발생하지 않도록 관리해야함

- 컴파일러는 에러가 정상적으로 핸들링 되었는지 알 수가 없음

 

Modern concurrency in swift

 

컴파일 타임, 런타임 모두에 밀접하게 동작 한다.

 

async

- 메서드나 함수가 비동기임을 명시

- 컴파일러가 동기 컨텍스트에서 호출이 불가하도록 강제

-  비동기 메서드가 결과를 반환할 때 까지 현재 코드의 실행을 일시 중지할 수 있음.

 

await

- asnyc 메서드가 리턴 되기를 기다리는 동안 일시 정지될 수 있음을 명시

 

Task

- 비동기 작업의 단위. 

- 작업이 완료될 때 까지 기다리거나 끝나기 전에 취소할 수 있음.

 

func fetchStatus() async throws -> ServerStatus {
    let (data, _) = try await URLSession.shared.data(
        from: URL(string: "https://amazingserver.com/status")!
    )
    return ServerStatus(data)
}

Task {
    viewModel.serverStatus = try await api.fetchStatus()
}

 

await 키워드를 사용하여 비동기 함수를 호출할 때 마다 런타임이 코드를 일시 중지하거나 취소할 기회 제공한다.

ㄴ 다른 작업을 실행하거나 필요에 따라 취소

 

이를 통해 시스템은 현재 작업 큐에서 우선순위를 계속하여 변경할 수 있다.

 

 

Separating Code into Partial Tasks

 

CPU 및 메모리 사용의 최적화를 위해 Swift는 중단점(Supsension Point)을 기준으로 코드를 나눈다.

이는 논리적인 단위로 partial tasks 또는 partials 라고 부른다.

위 빨간 박스 코드들이 모두 partial task이다. 

각각의 partial task가 완료되면, 시스템은 현재 코드를 이어서 진행할지, 다른 task를 수행할지 결정한다.

이는 런타임에 스케줄링 된다.

 

시스템의 결정에 따라 이 partial tasks들은 서로 다른 쓰레드에서 수행될 수 있다.

따라서 개발자는 await 이후의 앱의 상태(state)를 단언할 수 없다.

 

async/await은 간단한 syntax이지만 아주 강력하다.

컴파일러는 안전한 코드를 작성하도록 돕고, 런타임에서는 시스템 공유 자원의 사용을 최적화 한다.

 

Executing Partial Tasks

 

Swift 동시성 모델은 비동기 코드를 Partial tasks로 나누고 이를 Executor에서 실행한다.

 

 

Controlling a Task's Lifetime

 

 

이전 멀티 쓰레드 API들의 단점

- 비동기 코드가 시작되면 작업을 취소하기 어렵다.

- 예를 들면 서버 API를 두 번 요청 시 직전 요청을 취소하지 못하고 리소스를 그대로 낭비하게됨.

 

Modern Concurrency는 중단점 (Suspension Point)에서 작업을 취소할 수 있다.

또한 특정 Task를 취소할 경우, 하위 자식 task들을 모두 취소할 수 있다. (런타임에 이뤄짐)



중단점이 없는 장시간 지속되는 연산 작업 처리라면 ?

실행 중인 작업이 취소되었는는지를 확인할 수 있는 API를 제공하니 확인하고 수동으로 작업 취소 처리하면 됨.

 

 

 

에러 처리는 기존 throwing function과 유사하게 동작한다.

중단점이 탈출 경로 (esacpe route)를 제공한다.

가장 가까운 catch 구문에서 예외처리가 이뤄진다.

 

async-await syntax

 

function, computed property, closure에서 사용 가능

// Function
func myFunction() async throws -> String {
    ...
}

let myVar = try await myFunction()

// Computed Property
var myProperty: String {
    get async {
        ...
    }
}

print(await myProperty)

// closure
func myFunction(worker: (Int) async -> Int) -> Int {
    ...
}

myFunction(worker: {
    return await computeNumbers($0)
})

 

Grouping Asynchronous Calls

files = try await model.availableFiles()
status = try await model.status()

 

앞서 살펴본 것과 같이 위 코드는 순차적으로 수행된다.

즉, availableFiles()가 종료된 이후에 status() 가 수행된다.

 

각 메서드는 비동기이므로, 동시에 진행되기를 원할 수 있다.

 

이는 async let 으로 해결할 수 있다.

async let files = try model.availableFiles()
async let status = try model.status()

 

async let 바인딩은 local constant를 만들어준다.

바인딩 되는 즉시 비동기 코드는 수행된다.

await 키워드가 없으므로 각 라인에서 별도 supsepnd 처리 되지는 않는다.

 

바인딩된 값을 읽기 위해서는 await syntax가 필요하다.

결과 값이 비로 사용 가능하다면 해당 값을 즉시 얻을 것이고,

그렇지 않다면 결과 값을 얻을 때 까지 suspend 처리 된다.

 

값을 얻을 수 있는 방법은 두 가지가 있다.

- 배열 같은 컬렉션에 그룹핑

- 튜플로 묶기

let (fileResult, statusResult) = try await (files, status)
self.files = fileResult
self.status = statusResult

 

 

Task

Task {
    fileData = try await model.download(file: file)
}

 

- Task는 최상위 비동기 작업을 나타내는 타입

- 최상위라는 것은 특정 상위 작업 없이 독립적으로 실행되는 비동기 작업

- 동기 컨텍스트에서 바로 시작될 수 있다.

 

Task(priority:operation:) 

- 비동기 작업에 priority를 지정

- default 값은 현재 동기 컨텍스트의 것을 상속 받는다.

 

Task.detached(priority:operation)

- 호출된 컨텍스트의 default 값들을 상속받지 않는다.

 

Task.value

- 작업이 완료될 때 까지 기다린 후, 그 값을 반환

 

Task.isCancelled

- 마지막 중단점 이후 작업이 취소된 경우 true를 반환.

- 이 값을 검사하여 예정된 작업의 실행을 중지해야 할 시점을 인지 

 

Task.checkCancellation

- 작업이 취소됐으면 CancellationError를 던진다.

 

Task.sleep(:)

- 지정된 시간 동안 작업을 suspend

- 쓰레드가 block되는 것이 아님