관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - Swift] 1. Swift Concurrency 개념 - async, await, Task, async let, Actor, @MainActor, deadlock, nonisolated, DispatchQueue와 차이점 본문

iOS 응용 (swift)

[iOS - Swift] 1. Swift Concurrency 개념 - async, await, Task, async let, Actor, @MainActor, deadlock, nonisolated, DispatchQueue와 차이점

jake-kim 2022. 11. 29. 22:27

1. Swift Concurrency 개념 - async, await, Task, async let, Actor

2. Swift Concurrency 개념 - async, await를 이용하여 api 호출해보기

async, await 란?

  • iOS13+ 사용 가능
  • 비동기 처리를 위해서 completion closure를 사용하지 않고 async, await 키워드를 사용하여 더욱 가독성 있는 비동기 프로그래밍이 가능한 것

* 구체적인 async, await 내용은 이전 포스팅 글 async, await 사용 방법 참고

  • async 키워드) 함수 정의 부분, throws 앞에 사용
func fetchItems() async {

}

func fetchSomeItems() async throws -> String {

}
  • await 키워드) 사용하는 쪽에서 호출
await fetchItems()
await fetchSomeItems()

Task 개념

https://developer.apple.com/documentation/swift/task

  • Task는 async 작업의 단위이며, 스위프트에서 DispatchQueue나 OperationQueue, Thread 따로 없이 작성하면 main thread 인 sync로 동작 하는데, 이때 Task 블록으로 감싸서 async 코드를 수행할 수 있도록 제공
@frozen struct Task<Success, Failure> where Success : Sendable, Failure : Error

ex) sync에서 async 호출을 Task없이 async 함수 호출 시 에러 발생

  • await를 붙여도 에러 발생)

  • Task 블록으로 감싸게 되면 해결
Task {
  await fetchItems()
}
  • 만약 Task 블록 없이 async 함수를 호출하려면, async 함수를 호출하는 쪽도 async여야 가능
    • viewDidLoad()는 override 함수라, async를 붙이지 못하지만 이론상 async를 붙이면 Task 블록 없이도 호출 가능
  override func viewDidLoad() async { // <-
    super.viewDidLoad()
    
    await fetchItems()
  }

cf) 기능은 DispatchQueue.global().async로 호출하는 부분과 유사

class ViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    
    DispatchQueue.global().async {
      self.fetchHeavyItems()
    }
  }
  
  func fetchHeavyItems() {
    // get some heavy items
  }
}

ex) await 실행 순서

  override func viewDidLoad() {
    super.viewDidLoad()
    
    print("will enter task block")
    Task {
      print("did enter task block")
      try? await Task.sleep(for: .seconds(10)) // await를 만남 -> 다음 라인의 코드 실행시키지 않고 대기
      print("will out task block")
    }
    print("some another code")
  }

/*
will enter task block
some another code
did enter task block
will out task block
*/

Task와 DispatchQueue 스레드 차이

  • 공통점
    • DispatchQueue 블록 안에 들어가면 임의의 스레드로 처리
    • Task 블록 안에서도 임의의 스레드로 처리
  • 차이점
    • Task 블록 안에서 코드가 대기하여 다음 코드를 실행하지는 않지만, 내부적으로 스레드는 block 되지 않고 다른 일을 처리할 수 있도록 효율적으로 설계
  override func viewDidLoad() {
    super.viewDidLoad()
    
    Task {
      try? await Task.sleep(for: .seconds(10)) // await를 만남 -> 다음 라인의 코드 실행시키지 않고 대기
    }
  }
  • DispatchQueue는 해당 Thread가 점유하고 일을 하고 있으면, 그 스레드는 block되어 해당 일을 끝낼때까지 다른 일 처리를 못하는 상태
  override func viewDidLoad() {
    super.viewDidLoad()
    
    DispatchQueue.global().async {
      self.fetchHeavyItems()
    }
  }
  
  func fetchHeavyItems() {
    // get some heavy items
  }

async let 사용 방법 (Concurrency)

  • await로 데이터를 얻지 않고, async로 프로퍼티를 선언하여, 동시성 처리에 사용
async let a = getA() // async let 선언 방법

ex) 예제로 사용할 async 메소드 정의

  func getA() async -> Int {
    try? await Task.sleep(for: .seconds(3))
    return 3
  }
  
  func getB() async -> Int {
    try? await Task.sleep(for: .seconds(5))
    return 5
  }
  • async let 없이 호출할 경우 - 두 개 모두 동시에해도 되는 일이지만, 3초 대기 후 5초 대기 하여 총 8초간 대기하는 비효율적인 코드
// 8초 소요
Task {
  let a = await getA()
  let b = await getB()
  print(a + b)
}
  • async let을 사용하여 동시성 프로그래밍
// 5초 소요
Task {
  async let a = getA()
  async let b = getB()
  let sumAB = await (a + b) // <- 비동기 바인딩
  print(sumAB)
}

Actor 개념

  • 동시성 프로그래밍에서 가장 주의해야할 것인 Deadlock, race condition인데, 이 문제를 해결하기 위해 Swift에서 만들어낸 자료형
  • class와 같이 reference type이고 actor는 한 번에 하나의 작업만 액터의 상태를 변경할 수 있는 형태
    • mutex 역할을 내부에서 자동으로 처리
  • 사용 방법은 class로 정의하고, 초기화해서 사용하는 것과 동일
// actor 사용 방법
actor Counter {
  private let name: String
  private var count = 0
  
  init(name: String) {
    self.name = name
  }
    
  func plus() { 
    count += 1 
  }
}

let counter = Counter(name: "counter")

// 코드 참고: https://swiftsenpai.com/swift/swift-concurrency-get-started/

* Deadlock: 둘 이상의 프로세스가 다른 프로세스가 점유하고 있는 자원을 서로 기다릴 때 무한 대기에 빠지는 상황

(4가지 조건이 모두 충족되면 Deadlock 발생)

  • mutual exclusion - 자원 하나는 한 프로세스만 사용 가능
  • hold and wait - 하나 이상의 자원을 점유하면서 다른 프로세스가 점유하고 있는 자원을 대기하는 상태
  • no preemtion - 다른 프로세스에 할당된 자원은 사용이 끝날 때까지 뺏을수 없음
  • circular wait - 자원을 요구하며 대기하는게 순환적인 형태

Actor 사용 방법

  • actor 정의
actor Counter {
  private name: String
  private var count = 0
  
  init(name: String) {
    self.name = name
  }
    
  func plus() {
    count += 1
  }
}
  • 일반 인스턴스 메소드를 호출하듯이 actor의 메소드 호출은 불가능
    • 동시에 호출하는 멀티 스레드로부터 counter.plus()가 호출될 수 있으므로, race condition을 사전에 컴파일 에러로 막기 위해서 swift에서 에러 발생

  • actor의 메소드를 사용할땐 Task, await 키워드를 사용
    let counter = Counter(name: "counter")
    
    Task {
      await counter.plus()
    }

nonisolated 키워드

  • let키워드로 선언하여 읽기만 가능한 actor는 race condition이 발생하지 않는것을 보장하므로, 메소드 앞에 nonisolated를 붙여서 사용하는쪽에서 Task, await 키워드 없이 접근이 가능하게 제공
// nonisolated 키워드 예시
actor Counter {
  private let name: String
  private var count = 0
  
  init(name: String) {
    self.name = name
  }
    
  func plus() {
    count += 1
  }
  
  nonisolated func getName() -> String { // <-
    name
  }
}

// 사용하는쪽
let counter = Counter(name: "counter")
counter.getName() // Task, await 키워드 없이 접근 가능

@MainActor 키워드

https://developer.apple.com/documentation/swift/mainactor

  • 항상 main에서 동작하는 actor를 의미
  • 항상 메인 스레드에서 실행되어야 하는 클래스에다 @MainActor 키워드를 사용하여 구현
@MainActor
class SomeClass {
    
}
  • 특정 메소드만 메인 스레드에서 항상 실행되어야 하는 경우, 메소드 위에 @MainActor 키워드 사용
class SomeClass {
  @MainActor
  func runSomeMethod() {
  
  }
}
  • cf) Swift 5.5에서는 UIKit, SwiftUI 구성 요소가 모두 MainActor이므로, 백그라운드 작업이 완료된 후 UI를 업데이트 하려고 할 때 DispatchQueue.main.async {}로 수동 업데이트 필요 없이 자동으로 메인에서 동작

* 전체 코드: https://github.com/JK0369/ExAsyncAwaitConcurrency

* 참고

https://developer.apple.com/documentation/swift/mainactor

https://developer.apple.com/documentation/swift/actor

https://developer.apple.com/documentation/swift/task

https://developer.apple.com/tutorials/app-dev-training/modernizing-asynchronous-code

https://swiftsenpai.com/swift/swift-concurrency-get-started/

Comments