본문 바로가기

Swift/Concurrency

WWDC 2022 - Eliminate data races using Swift Concurrency 정리하기

Concurrency - 바다

  • Task - 보트
    보트(Task)간 통신엔 Sendable 프로토콜을 준수하는 타입만 가능하다.


  • Sendable
    Sendable은 말 그대로 보낼수 있는 상태를 의미함 → data race를 발생시키지 않고 안전하게 공유할 수 있는 데이터 타입
     -> Struct, Enum 과 같은 값타입들을 포함하는 것들

    참조타입(Class)은 제한된 경우(final class가 immutable storage만을 가진 상황) 를 제외하곤 Sendable일 수 없다.
    - 이를 무시하기 위해서 @unchecked Sendable을 사용할 수 있다. 물론 문제에 대한 책임또한 생긴다.

    Task 클로저 내부엔 Sendable이 명시되어 있기 때문에 새로운 클로저를 생성할 때 이전 컨텍스트에서 무언가를 캡처하는 경우, Sendable을 검사한다.

    이렇게 공유하는 데이터간 안전성을 Sendable을 통해서 보장했다. 그러나 하나의 공유 데이터를 여러 Task에서 접근하게 될 경우 문제가 발생할 수 있다.


  • 이것을 해결하기 위한 Actor - 섬
    Actor는 서로 다른 task에서 공유 데이터에 접근하는 방법을 data race를 발생시키지 않고 조정하는 방식으로 제공한다.
    이 섬 내부에 데이터들은 모두 독립적이고, 이 독립적인 데이터를 핸들링하기 위해선 섬 내부(Actor isolated) 에서만 핸들링해야 한다
    → actor 내부 메서드만 actor의 인스턴스에 직접 접근할 수 있다.
    Task(보트)에서 actor(섬)과 상호작용하기 위해선 섬 앞에 await 키워드를 붙여줘야한다 - actor은 동시적인 접근을 제한하고 한번에 하나의 Task만 상호작용하기 때문에 다른 Task 는 대기해야 한다.

    당연히 non-Sendable 타입은 상호작용할 수 없다.

    - actor에 대한 참조 - 섬에 대한 지도

    • Actor isolated
      actor 외부는 당연히 nonisolated 하기 때문에 내부 프로퍼티에 직접 접근이 불가능하고, 외부에서 await 키워드를 통하여 간접적으로만 접근 가능하다.
      actor 내부의 함수를 nonisolated 키워드를 사용해 actor 외부로 옮길 수 있다 - 근데 왜?
      1. 액터 외부에서 필요한 비동기 작업: 액터 내부의 함수가 액터 외부에서 호출되어야 하고, 해당 작업이 액터의 Isolated context와 직접적인 연관이 없을 때 유용합니다. 예를 들어, 액터가 네트워크 요청을 처리하는 함수를 갖고 있을 수 있습니다. 이 함수는 액터 외부에서 호출되어야 하며, 네트워크 요청은 액터의 Isolated context와 독립적으로 수행될 수 있습니다. 이 경우 nonisolated 키워드를 사용하여 액터 외부로 함수를 옮길 수 있습니다.
      2. 액터 내부 상태 변경이 필요 없는 경우: 액터 내부의 함수가 액터의 상태를 변경하지 않고 단순히 연산 또는 결과 반환에만 관여하는 경우에도 nonisolated 키워드를 사용하여 액터 외부로 함수를 이동할 수 있습니다. 이는 액터의 Isolated context를 필요로 하지 않는 작업을 다른 곳에서 처리하기 위한 방법입니다.

        이상 Chat GPT의 대답

    • MainActor - 바다의 중간에 있는 큰 섬
      유저 인터페이스와 관련된 메인 스레드에서 실행되는 Actor


  • Atomicity
    • 고수준의 data races를 막기위한 방법 - 고수준의 data races는 예시 참조
    외부에서 actor에 접근함에 있어 await 을 통한 간접적인 내부 프로퍼티 접근(non-isolated)을 지양한다.액터 내부에서 동기적이고 Transaction 형태의 작업을 고려해야한다.
    → await 구문은 작업이 어디선가 일시 중단될 수 있고, actor가 다른 높은 우선순위의 작업을 처리하며 예사이 못한 데이터 손상을 발생시킬 수 있다.



  •  Ordering - 일관된 순서로 이벤트를 처리하고싶다
    Swift Concurrency는 작업을 순서대로 처리하기 위한 도구를 제공하지만, actor은 아니다.
    Actor은 가장 높은 우선순위의 작업을 먼저 실행하여, 전체 시스템이 반응적으로 유지되도록 돕는다.
    Task 는 제어 흐름을 따라 시작부터 끝까지 실행되므로 자연스럽게 작업을 순서대로 처리할 수 있다.
    AsyncStream은 실제 이벤트 스트림을 모델링하는 데 사용할 수 있다. 하나의 Task가 for-await-in 루프를 통해 스트림의 이벤트들을 반복하면서 각 이벤트를 차례대로 처리할 수 있다.


  • Sendability
    Swift 5.7에선 Sendability를 엄격하게 체크할 수 있는 빌드 설정이 도입되었다
    → Strict Concurrency Checking
    • Minimal - 기본 설정
      Sendable로 표시한 경우에만 오류가 아닌 “경고” 로 제공됨
    • Targeted - 기존 코드와의 호환성과 잠재적인 경쟁을 식별하는 것 사이의 균형을 유지하기
      async/await, tasks, 또는 actors와 같은 Swift Concurrency 기능을 이미 채택한 코드에 대해 Sendable 체크를 활성화한다.
      @preconcurrency 속성을 통해 임시적으로 경고를 비활성화 할 수 있다.
    • Complete - 잠재적인 모든 데이터 경쟁을 드러내기
      점진적으로 엄격한 동시성 검사를 통한 오류를 제거하는 방식으로 data race safety한 앱을 달성하도록 권장한다.