Skip to content

[승용] Eliminate data races using Swift Concurrency

Eric Kwon / 권승용 edited this page Sep 2, 2024 · 1 revision

공식문서

  • Swift Concurrency는 동시성 프로그램을 쉽게 작성하는 일련의 Swift 언어 기능
  • 데이터 경쟁 없이 동시성을 효율적으로 활용하도록 프로그램을 구성하기 위한 방법으로 Swift Concurrency 살펴보기
  • 격리(isolation)는 Swift Concurrency 모델의 핵심 아이디어 중 하나
  • 잠재적인 Data race가 일어나지 않는 방식으로 데이터가 공유되도록 보장한다.

Task Isolation

  • Task는 시작부터 끝까지 순차적으로 작업을 수행함

  • 비동기적으로 실행되며 await 키워드를 만나면 언제든지 작업이 중단(suspend)될 수 있음

  • 독립적임(Self-contained)

  • 동시성의 바다에 떠다니는 보트로 생각할 수 있음

  • 어떤 데이터를 각 보트 간에 전달해야 하는데, 값 타입은 복사본을 생성해서 전달

  • 그래서 변경이 가해져도 각 보트 안에서만 변경됨 -> 변경이 local하기 때문에 데이터 경쟁 없음

  • 참조 타입은 참조를 전달, 두 보트가 같은 데이터를 가리키고 있음 -> 보트 A의 데이터 변경이 보트 B의 데이터에도 영향을 미침 -> 데이터 경쟁 가능성 있음 -> 보트가 독립적이지 않게 됨

  • 따라서 각 Task들끼리 공유해도 안전한 타입과 그렇지 않은 타입을 추론할 수 있는 방법이 있어야 함

  • 여기서 Sendable 프로토콜 사용!

  • Sendable 프로토콜로 공유 안전한 타입을 모델링 가능

  • 서로 다른 격리된 Task 사이에 제네릭으로 데이터가 전달된다면 Sendable 준수해야 함

  • Sendable 체킹을 통해 Task들 사이에 Sendable로 전달하는지 컴파일러가 감시 -> 컴파일러가 각 Task가 isolated하게 유지되는지 감시한다는 의미

  • 프로퍼티가 Sendable하면 타입도 Sendable

  • 배열의 요소가 Sendable하면 배열도 Sendable

  • 클래스는 final class가 불변프로퍼티만을 지닐 경우 등 특별한 경우만 Sendable할 수 있음

  • Task의 클로저는 독립적으로 실행되기 때문에 Sendable한 값만 캡처할 수 있음

    • 이는 Task의 구현에 @Sendable로 나타나 있음
  • @Sendable은 함수 타입으로 전송 가능한 함수임을 나타냄

  • 공유해야 하는 값은 Sendable 사용하기!

Actor Isolation

  • 근데 Sendable은 변경 불가능한 데이터 이야기.

  • Data race가 없으면서도 shared mutable state를 공유할 방법이 필요함. -> Actor!

  • Actor는 동시성의 바다에서 섬으로 생각할 수 있음

  • 다른 모든 것과 격리되어 고유한 상태를 가짐

  • Actor를 실행하려면 Task가 필요

  • 한 번에 하나의 Task만이 Actor에 접근 가능

  • 다른 Task는 기다릴 수 있기 때문에 await 키워드로 표시된 지점이 잠재적 suspention point

  • Task와 Actor 사이에서도 non-Sendable 타입이 교환되지 않도록 격리해야 한다.

  • Actor-isolated 한 component들

  • Actor의 인스턴스 프로퍼티 / 메소드 / extension의 메소드 / 전송 불가능한 클로저 / Actor context 내부의 Task

  • Detached Task와 같은 독립적인 맥락(context)에서 생성된 녀석은 actor-nonisolated

  • nonisolated 키워드를 사용하면 어떤 Actor에서도 실행되지 않는 코드 만들 수 있음

  • nonisolated async 코드는 항상 global cooperative pool에서 작동한다.

    • 동시성의 바다에서 보트가 공해에 나와 있을 때만 작동한다고 생각.
    • 그래서 Actor로부터 전송 불가능한 데이터를 갖고 있지는 않은지 고려.
    • 섬(Actor)에서 전송 불가능한 데이터를 가지고 공해로 나올 수 없다.
  • Actor에 진입하거나 빠져나올 때 Sendable 확인이 컴파일러에 의해 자동적으로 이루어진다.

  • Actor 자기 자신도 Sendable하다.

  • MainActor는 프로그램의 UI와 관련된 상태를 많이 가지고 있다

  • 많은 UI 프레임워크 코드와 앱 코드가 MainActor 위에서 실행되어야 한다

  • 그러나 한 번에 하나의 작업만 실행되어야 한다.

  • MainActor의 사용으로 해당 코드가 메인 스레드에서 실행됨을 Swift가 보장하며, Actor와 같이 MainActor가 작업중일 땐 다른 작업자는 접근할 수 없다.

  • 따라서 MainActor로부터 nonisolated 된 context에서 MainActor 코드를 호출하는 경우 await이 필요하다.

Atomicity

  • Actor는 한 번에 하나의 작업만 수행한다.
  • 작업이 끝나면 Actor는 다른 작업을 수행할 수 있다.
    • 프로그램이 계속 진행되므로 Deadlock의 가능성을 피할 수 있음
  • 이러한 과정으로 low-level Data race는 피할 수 있다.
  • 그러나 await 구문 전후의 상태 변화에 의해 High-level Data race가 발생할 수 있다.
  • 이를 피하기 위해서 Actor에 대한 상태 변화는 Actor 내부에서 동기적인것이 좋고
  • nonisolated async 함수 내부에서의 await 전후로 상태변화가 있을 수 있음을 고려해서 코드를 작성해야 한다.
  • transactionally 하게 생각하기
    • Identify synchronous operations that can be interleaved
    • keep async actor operations simple

Ordering

  • 동시성 프로그램은 여러 가지 작업들이 동시에 진행되기 때문에 작업들의 실행 순서는 실행마다 다를 수 있다.

  • 그러나 일관된 순서로의 처리가 필요한 작업들도 있다.

  • Actor는 작업 순서를 지정할 수 없다. Actor는 전체 시스템의 응답성을 유지하기 위해 우선순위가 가장 높은 작업을 먼저 처리해서 우선순위 역전을 방지한다.

  • Swift Concurrency에서 순서 부여를 위한 도구들

    • Task -> 코드를 순서에 따라 실행한다
    • AsyncStreams -> 이벤트 스트림 모델링 가능
  • Sendable이 도입되었지만 모든 부분에 Sendable을 한 번에 적용하기는 어렵다. 점진적으로 적용해야 한다.

  • 그래서 컴파일러 옵션으로 얼마나 엄격하게 Sendable을 적용해야 하는지를 설정 가능함.

  • 다른 모듈에서 Sendable을 지원하지 않을 수도 있음 -> 오류가 날텐데, @preconcurrency 를 import 앞에 붙여서 오류 제거할 수 있음

  • Complete checking -> 모든 Data race 검사

Clone this wiki locally