Swift의 Concurrency

Thread & Queue

swift-concurrency
  • Task: 일
  • Thread: 노동자
  • Queue: 대기 행렬 (FIFO 구조)

GCD: Grand Central Dispatch

swift-concurrency
  • GCD는 Dispatch Queue 사용: OS가 Thread로 알아서 분산 처리

Dispatch Queue 종류

  • Main Queue: 1개, Serial, Main Thread
  • Global Queue: Concurrent, QoS(Quality Of Service) 종류 6개
  • Custom Queue: Serial or Concurrent 선택 가능, QoS 설정 가능

Operation Queue

  • GCD + @ 라고 생각하면 됨
  • 동시에 실행할 수 있는 동작의 최대 수 지정 가능
  • 동작 일시 중지 및 취소 가능

Sync vs Async

  • Sync: 큐에 보낸 작업이 완료 될 때까지 기다림
swift-concurrency
  • Async: 큐에 보낸 작업을 안 기다리고 다음 코드 실행 (끝나기를 기다리지 않음)
swift-concurrency
  • Sync vs Async: 작업 보내는 시점에서 끝나기를 기다릴지 말지

Serial vs Concurrent

  • Serial queue (직렬 큐): 한개의 쓰레드에서 전부 진행 → 순서 예측 가능
swift-concurrency
  • Concurrent queue (병렬 큐): 여러개의 쓰레드에서 진행 → 순서 보장 X
swift-concurrency
  • Serial vs Concurrent: 큐 보낸 작업들을 한개 vs 여러개 쓰레드로 보낼지

Async & Await

swift-concurrency
  • Await은 potential suspension point로 지정된다는 것이 중요
  • Suspend 된다는 것은 해당 thread에 대한 control을 포기한다는 것
  • 원래 코드를 돌리고 있던 thread에 대한 control은 system에게 간다
  • system은 해당 thread를 사용하여 다른 작업을 수행할 수 있게 된다
swift-concurrency
  • 즉, await로 인한 중단은 해당 thread에서 다른 코드의 실행을 막지는 않음
  • await 키워드를 통해 code block이 하나의 transaction으로 처리되지 않게는 할 수 있다
  • function은 suspend 되고, 다른 것들이 먼저 실행 될 수 있기 때문에 일시 중단 되는 동안 앱의 상태가 크게 변할 수 있음
  • thread를 차단하는 대신, thread의 control을 포기하고 작업을 중지 및 재개할 수 있는 개념을 도입한것

Task

  • 비동기 작업 단위: A unit of asynchronous work
  • 격리되어(isolated), 독립적으로(independently) 비동기 작업을 수행
  • 값이 공유될 상황이 있을때는 Sendable 체킹을 통해 Task가 격리된 상태로 남아있는지 체크
swift-concurrency
  • Task 안에서의 작업은 처음부터 끝까지 순차적으로 실행
  • await를 만나면 작업은 몇번이고 중단될 수는 있지만, 실행 순서가 변경되지는 않음
swift-concurrency
  • Data Race: 여러 Task가 동시에 일을 하고 있지만 동일한 객체(Class)를 참조하기 때문에 발생
  • Sendable 프로토콜: 동시에 사용해도 안전한 타입 (Actor)

Actor

  • 공유 데이터에 접근해야 하는 여러 Task를 조정
  • 외부로부터 데이터를 격리하고, 한 번에 하나의 Task만 내부 상태를 조작하도록 허용
  • 동시 변경으로 인한 Data Race를 피함
  • actor 목적이 공유되는 가변 상태(shared mutable state)를 표현하는 것이기 때문에 class와 같은 reference 타입
actor SharedWallet {
    let name = "공유 지갑"
    var amount = 0
    
    init(amount: Int) {
        self.amount = amount
    }
    
    func spendMoney(ammount: Int) {
        self.amount -= ammount
    }
}

Task {
    let wallet = SharedWallet(amount: 10000)
    let name = wallet.name // 1 
    let amount = await wallet.amount // 2
    await wallet.spendMoney(ammount: 100) // 3
    await wallet.amount += 100 // 4
}
  • 1: 상수는 변경 불가능하기 때문에 어느 스레드에서 접근해도 안전하고 actor 외부에서도 바로 접근 가능
  • 2: actor 외부에서 변수 접근시 await 필요
  • 3: actor 외부에서 메서드 호출시 await 필요
  • 4: 컴파일 에러, actor 외부에서 actor 내부의 변수를 변경할 수 없음

MainActor

  • MainActor: main thread를 나타내는 특별한 global actor
  • await이 사용되었다는 것은, 해당 thread에서 다른 코드가 실행될 수 있도록 실행 중인 함수가 중지될 수 있다는 의미
await MainActor.run {
 // UI 관련 코드 1
}
await MainActor.run {
 // UI 관련 코드 2
}
  • 따라서 메인 스레드에서 한꺼번에 작업이 이뤄지길 원하는 경우에는 관련 함수를 run block에 그룹화 해야함
  • 그래야 해당 함수들 사이에는 일시 중단 없이 호출이 실행되도록 할 수 있음
await MainActor.run {
 // UI 관련 코드 1
 // UI 관련 코드 2
}
  • Main Thread에서 실행되어야 하는 코드를 @MainActor 표시로 함수, 클로저, 타입 등에 적용 가능
  • class, struct, enum 같은 type에 붙으면, 내부에 있는 모든 property와 method가 isolated 되고 main thread에서 동작
actor SomeActor {
    let id = UUID().uuidString
    @MainActor var myProperty: String
    
    init(_ myProperty: String) {
        self.myProperty = myProperty
    }
    
    @MainActor func changeMyProperty(to newValue: String) {
        self.myProperty = newValue
    }
    
    func changePropertyToName() {
        Task { @MainActor in
            myProperty = "naljin"
        }
    }
}

참고

사진 출처 및 참고한 자료