study record

[Swift Concurrency] Getting Started with async/await 본문

Swift/Concurrency

[Swift Concurrency] Getting Started with async/await

asong 2023. 5. 21. 17:54
  • async: 메서드가 비동기로 작동할 것임을 보여준다. 이 메서드가 결과를 리턴할 때까지 실행을 멈춘다.
  • await: 코드가 실행을 멈출 수 있음을 보여준다. async 메서드가 리턴할 때까지 기다린다.
  • Task: 비동기 작업의 단위. 작업이 끝나거나 취소되는 것을 기다릴 수 있다.

 

🧪 completion handler → async 메서드로 변경해보기

// escaping closure
func fetchStatus(completion: @escaping (String) -> Void) {
  URLSession.shared.dataTask(
    with: URL(string: "<http://amazingserver.com/status>")!
  ) { data, response, error in
      guard let data = data else { return }
      completion(data.description)
  }
  .resume()
}

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

→ escaping closure를 활용한 메서드에서는 따로 에러 핸들링을 하지 않아도 컴파일 에러가 발생하지 않는다.

async 메서드에서는 error handling이 되지 않고 있다는 컴파일 에러를 확인할 수 있다.

 

Separate Code Into Partial Tasks

시스템은 await 키워드테스크를 분리하고, 각 테스크가 완성될 때 코드를 계속 이어나갈지, 다른 작업을 실행할지 결정한다.

그래서 await 키워드를 사용할 때 시스템의 결정에 따라 다른 스레드에서 각 테스크가 실행될 수 있음을 기억해야 한다.

 

Control a Task’s Lifetime

modern concurrency의 새 특징은 시스템이 비동기 코드의 생명 주기를 관리한다는 것이다.

코드를 분리하고 suspension point를 제공하여 코드를 중지하거나 취소할 수 있는 기회를 준다.

한 테스크를 취소할 때, 런타임은 비동기 계층을 따라가 하위 테스크를 모두를 취소한다.

 

Grouping Asynchronous Calls

동시에 비동기 호출을 하고자 할 때(서로 의존적이지 않을 때) async let을 사용한다.

이 피쳐는 structured concurrency라고 불린다

 

🧪 async let 실제로 동시에 시작해서 끝나는가?

Task {
    do {
        async let files = try availableFiles()
        async let status = try getStatus()
        let (filesResult, statusResult) = try await (files, status)
    } catch {
        print("\\(error.localizedDescription)")
    }
}

각 메소드의 호출 시간을 체크해 본 결과 거의 동시에 호출되는 것을 확인할 수 있었다.

 

Example Note

비동기적으로 데이터 가져오는 작업을 진행해야 한다. 그 이유는 시스템이 그래야 스레드로 하여금 다른 일을 할 수 있게 하기 때문이다. 응답을 기다릴 떄까지. 공유 시스템 자원 사용을 다른 작업으로부터 막지 않는다.

 

await 키워드를 볼 때마다 suspension point 임을 생각해야 한다.

현재의 코드가 실행을 멈출 것이고 다른 높은 우선순위 작업이 실행되게 할 것이다.

 

메서드의 실행은 전체적으로 비동기적이지만, 코드는 동기적으로 읽힌다. 그래서 상대적으로 이해하고 유지보수하기 쉬워진다.

 

.task 는 view modifier이다. 뷰가 나타날 때마다 비동기 코드를 실행할 수 있게 한다. 또한 뷰가 사라질 때 비동기 실행을 취소하는 것도 다룬다.

 

async, await, let 구문으로 하여금 non-blocking asynchronous code가 실행이 가능해졌다. serially, in parallel 에서도.

 

Task를 통한 빠른 우회

Task 는 top-level 비동기 테스크를 표현하는 타입이다.

top-level이 되는 것은 비동기 컨텍스트를 만들 수 있다는 것을 의미한다.

  • Task(priority:operation): Schedules operation for asynchronous execution with the given priority. It inherits defaults from the current synchronous context.
  • Task.detached(priority:operation): Similar to Task(priority:operation), except that it doesn’t inherit the defaults of the calling context.
  • Task.value: Waits for the task to complete, then returns its value, similarly to a promise in other languages.
  • Task.isCancelled: Returns true if the task was canceled since the last suspension point. You can inspect this boolean to know when you should stop the execution of scheduled work.
  • Task.checkCancellation(): Throws a CancellationError if the task is canceled. This lets the function use the error-handling infrastructure to yield execution.
  • Task.sleep(for:): Makes the task suspend for at least the given duration and doesn’t block the thread while that happens.

높은 우선순위 컨텍스트에서 낮은 우선순위 테스크를 만들 때에는 우선순위를 설정해야 한다.

 

메인 스레드로의 코드 Routing

await 키워드로 하여금 suspension point가 되고, 코드가 다른 스레드에서 멈출지도 모른다. 메인 액터 위에 실행되던 테스크가 있어 메인 스레드에서 코드의 첫 조각이 실행될 수 있다. 하지만 첫 await 이후에, 코드는 다른 스레드에서 실행될 수 있다. 따라서 main actor로 UI 코드를 명확하게 라우팅해야한다.

 

메인 스레드로 코드를 확실하게 실행하는 방법 중 하나는 MainActor.run()을 호출하는 것이다.

MainActor는 코드를 메인 스레드 위에 실행하게 하는 타입이다. DispatchQueue.main의 modern 대안이라할 수 있다.

하지만 MainActor.run()의 경우 많은 클로저를 낳게 된다.

 

더 우아한 해결책은 특정 메소드나 클로저에 @MainActor를 붙이는 것이다.

해당 범위의 작업이 main actor에서 이루어질 것이다.

 

🧪 @MainActor가 메인 스레드에서 동작하는가?

func download(file: DownloadFile) async throws -> Data {
    guard let url = URL(string: "<http://localhost:8080/files/download?\\(file.name)>") else {
        throw "Could not create the URL."
    }
    print("before addDownload() : \\(Thread.isMainThread)") // false
    await addDownload(name: file.name)
    print("after addDownload() : \\(Thread.isMainThread)") // false
    
    let (data, response) = try await URLSession.shared.data(from: url, delegate: nil)
    
    print("before updateDownload() : \\(Thread.isMainThread)") // false
    await updateDownload(name: file.name, progress: 1.0)
    print("after addDownload() : \\(Thread.isMainThread)") // false
    
    guard (response as? HTTPURLResponse)?.statusCode == 200 else {
        throw "The server responded with an error."
    }
    return data
}

@MainActor func addDownload(name: String) {
    print("addDownload() : \\(Thread.isMainThread)") // true
    
    let downloadInfo = DownloadInfo(id: UUID(), name: name, progress: 0.0)
    downloads.append(downloadInfo)
}

@MainActor func updateDownload(name: String, progress: Double) {
    print("updateDownload() : \\(Thread.isMainThread)") // true
    
    if let index = downloads.firstIndex(where: { $0.name == name }) {
        var info = downloads[index]
        info.progress = progress
        downloads[index] = info
    }
}

→ @MainActor를 붙이지 않은 경우, mainThread에서 작동하지 않는다.

func download에서는 Thread.isMainThread가 false로만, @MainActor를 붙인 메소드에서는 true로 찍히는 것을 볼 수 있다.

 

🧪 @MainActor와 DispatchQueue.main과 똑같은가?

func addDownload(name: String) async {
    DispatchQueue.main.async {
        print("addDownload() : \\(Thread.isMainThread)")
        let downloadInfo = DownloadInfo(id: UUID(), name: name, progress: 0.0)
        downloads.append(downloadInfo)
    }
}

func updateDownload(name: String, progress: Double) async {
    DispatchQueue.main.async {
        print("updateDownload() : \\(Thread.isMainThread)")
        if let index = downloads.firstIndex(where: { $0.name == name }) {
            var info = downloads[index]
            info.progress = progress
            downloads[index] = info
        }
    }
}

→ DispatchQueue.main.async로 바꾼 경우, 메인 스레드에서 작동하는 것은 똑같으나 기본으로 suspension point를 보장하지 않는다.

 

@MainActor

UI 업데이트와 관련있는 메소드들 앞에 어노테이션을 붙인다.

자동적으로 메인 액터 위에 메소드들이 실행되어 메인 스레드 위에서 실행된다.

이 어노테이션은 async 키워드를 뒤에 붙이지 않아도 비동기적으로 작동한다.