본문 바로가기
iOS/Swift

Protect mutable state with Swift actors

by 바등쪼 2023. 8. 31.

https://developer.apple.com/videos/play/wwdc2021/10133/

 

Protect mutable state with Swift actors - WWDC21 - Videos - Apple Developer

Data races occur when two separate threads concurrently access the same mutable state. They are trivial to construct, but are notoriously...

developer.apple.com

 

동시성 프로그래밍을 할 때 근본적으로 어려운 문제 중 하나는 Data race를 피하는 것이다.

 

Data race는 두 개의 개별 스레드가 동시에 동일한 데이터에 엑세스하고 이러한 접근 중 적어도 하나가 write인 경우에 발생한다.

Data race는 trival 하지만 디버깅하기 어렵기로 악명 높다.

Data race 예시

class Counter {
    var value = 0

    func increment() -> Int {
        value = value + 1
        return value
    }
}

let counter = Counter()

Task.detached {
    print(counter.increment()) // data race
}

Task.detached {
    print(counter.increment()) // data race
}

2개의 동시 작업에서 increment 함수를 실행하고 있다.

 

실행 시기에 따라 결과 값이 1, 2 또는 2, 1이 될 수 있다.

 

하지만 Data race에 의해 두 작업 모두 0을 읽고 1을 write하면 1, 1이 나올 수도 있다.

혹은 둘 다 1이 증가된 값을 읽어와서 2, 2를 리턴할 수도 있다.

 

데이터 경쟁(Data race)은 피하기 어렵고 디버그하기 어렵다.

 

Data race를 유발하는 데이터 액세스가 프로그램의 다른 부분에 있을 수 있기 때문에 nonlocal reasoning이 필요하다.

프로그램을 실행할 때마다 운영체제의 스케줄러가 concurrent tasks를 다른 방식으로 interleave할 수 있기 때문에 이러한 작업은 nondeterministic하다.

 

Data race는 shared mutable state에 의해 발생한다.

 

데이터가 변경되지 않거나 (no-write) 여러 동시 작업에서 공유되지 않으면 데이터 경젱은 발생할 수 없다.

 

Data race를 피하는 1가지 방법은 Value Sementics를 사용하여 공유된 가변 상태를 제거하는 것이다. (값타입 사용)

 

Value Type의 변수의 모든 mutation은 Local이기 때문이다.

또한 Value-Sementic 타입을 "let"으로 선언하면 실제로 변경할 수 없으므로 다양한 Concurrent tasks에서 액세스해도 안전하다.

 

Swift는 Value semantics를 홍보해 왔는데 이는 우리의 프로그램에 대해 더 쉽게 추론할 수 있게 해주고 동시 프로그램에서 안전하게 사용할 수 있게 해주기 때문이다.

 

Value Type인 Array 사용 예시

 

Value semantics 적용

앞선 예시에서 Counter 클래스를 값 타입인 구조체로 바꾸었다.

let으로 counter를 선언하면 위와 같이 컴파일 에러가 발생한다.

 

따라서 var로 선언을 바꾸어 보지만 역시 컴파일 에러가 발생한다.

왜냐하면 두 가지 동시 작업에 의해 counter가 참조되기 때문에 다시 Data race 상태가 되기 때문이다.

 

각 concurrent task 내부의 local mutable 변수에 counter를 할당하면 더 이상 컴파일 에러가 발생하지 않는다.

즉, Data race는 발생하지 않는다.

 

하지만 두 작업 모두 1을 리턴하게 되고 이 결과는 우리가 원하는 값이 아니다. (각각 1과 2를 리턴하길 원함)

이는 여전히 shared mutable state가 필요하다는 것을 보여준다.

 

동시성 프로그래밍에서 공유된 가변 상태(shared mutable state)가 있는 경우, Data race를 야기하지 않도록 하기 위해 Synchronization이 필요하다.

 

Atomic, Locks과 같은 Low-level tools과  Serial dispatch queue와 같은 High-level 요소들이 동기화를 제공한다.

 

이러한 것들은 다양한 장점을 가지고 있지만 동일한 cirtical한 약점을 공유한다.

매번 정확히 사용하려면 세심한 훈련이 필요하는 점이다. 그렇지 않으면 data race가 발생한다.

(프로그래머의 실수가 발생할 여지가 생김)

 

➡️ Actor가 등장!!!!

 

 

Actors

  • Actor는 공유 가변 상태에 대한 동기화 메커니즘이다.
  • Actor는 자신의 상태(state)가 있고 그 상태는 프로그램의 나머지로부터 격리된다. (isolate)
  • 그 상태에 접근할 수 있는 유일한 방법은 actor를 통하는 것이다.
  • Actor에 접근할 때마다 Actor의 동기화 메커니즘은 다른 코드가 Actor의 상태에 동시에 액세스하지 않도록 보장한다.
  • 이것은 lock이나 serial dispatch queues를 사용하여 수동으로 mutual exclusion을 취하는 것과 동일하지만 Actor의 경우는 Swift가 제공하는 fundamental guarantee이다.
  • Synchronization을 수행하는 것을 잊으면 Swift가 컴파일 에러를 발생시킨다.

 

  • Actor는 Swift의 새로운 타입이다.
  • Actor는 Swift의 다른 named types들과 같은 기능을 제공한다.
  • 속성, 메서드, 생성자, subscripts 등을 가질 수 있다.
  • 프로토콜 채택과 extension 또한 가능하다.
  • Class처럼 참조 타입이다. (Reference Type)
    • Actor의 목적이 공유된 가변 상태를 표현하는 것이기 때문!
  • Actor 타입의 가장 큰 특징은 인스턴스 데이터를 나머지 프로그램으로부터 격리하고 해당 데이터에 대한 동기화된 액세스를 보장한다는 것이다.
  • 앞선 예제인 Counter를 Actor로 교체했다.
    • 이 경우, increment 메서드가 호출되면 Actor에서 다른 코드가 실행되지 않고 increment 함수가 완료될 때까지 실행된다. (Atomic 하게 함수가 실행된다는 의미인 듯!) 
  • 이러한 보장은 Actor 상태에 대한 Data race의 가능성을 제거한다.

 

이렇게 actor로 Counter를 선언하고 실행하면

1,2 또는 2,1 의 결과를 얻을 수 있다. (우리가 원하던 결과)

 

Actor의 내부 동기화로 Actor State에서 Data race가 발생할 가능성이 사라졌기 때문에 1,1 또는 2,2와 같은 결과가 발생하지 않는다.

 

 

이 예제에서 두 개의 concurrent tasks이 counter를 increment 시킬 때 실제로 어떤 일이 발생하는지 알아보자!

한 Task가 먼저 실행되면 다른 Task는 차례를 기다려야 한다.

Swift가 이러한 메커니즘을 지원한다.

 

  • 외부에서 Actor와 상호 작용할 때 마다 비동기적으로 수행한다.
  • Actor가 busy 하다면 우리의 코드는 suspend되어 현재 실행중인 CPU가 다른 유용한 작업을 수행할 수 있다.
  • Actor가 다시 자유로워지면 코드가 활성화되고 실행이 재개된다.
  • await 키워드는 Actor에 대한 비동기 호출에 이러한 중단(suspension)이 포함되어 있을 수 있음을 나타낸다.

 

  • 예시를 위해 value를 0으로 바꾸고 1씩 증가시켜서 newValue와 동일해지는지 검사하는 함수를 만들었다.
  •  Actor 내부에 함수를 만들었기 대문에 동기화 메커니즘이 작동한다.
  • 즉, 이 함수는 중단되지 않고 항상 완료될 때까지 실행된다. (실행 완료를 보장 == uninterrupted)
  • 따라서, 동시성이 우리의 Actor 상태에 미치는 영향을 고려할 필요 없이 동기 코드에 대해 순차적으로 추론할 수 있다.
  • 우리의 동기 코드가 중단되지 않고 실행된다는 것을 강조했지만, Actor는 종종 서로 또는 시스템의 다른 비동기 코드와 상호작용한다.

Actor와 비동기 코드

  • 이미지 다운로드 Actor이다.
    • 캐시를 확인하고 이미지를 다운로드 받고 이미지를 캐시에 기록한 다음 반환한다.
  • Actor이기 때문에 이 코드는 low-level의 Data race에서 자유로우며, 임의의 수의 이미지를 동시에 다운로드 할 수 있다.
  • Actor의 동기화 메커니즘은 캐시 인스턴스 속성에 액세스하는 코드를 한번에 하나의 작업만 실행할 수 잇음을 보장하므로 캐시가 손상될 일은 없다.
  • 중간에 있는 await 키워드는 매우 중요하다.
    • 함수가 해당 지점에서 suspend 될 수 있다는 의미이다.
    • CPU 제어권을 포기하여 프로그램의 다른 코드가 실행될 수 있도록 하여 전체 프로그램 상태에 영향을 미친다.
    • 함수가 Resume 되는 시점에서 전체 프로그램 상태가 변경되었을 것이다.
    • await 이전의 상태와 await 이후의 상태가 다를 수 있다는 것을 반드시 인지해야 한다!!

비동기 코드로 인한 상태 변화 예시

  • 동일한 이미지를 동시에 가져오려고 하는 예시이다. (캐시가 비어있다고 가정)
  • Task1과 Task2가 동일한 이미지 URL로 요청을 보내 이미지를 가져온다.
  • Task1이 실행되고 await 키워드를 만나 suspend 된다.
  • Task1이 이미지를 다운로드 하는 동안 서버에서 해당 URL에 대한 이미지가 변경될 수 있다. (웃는 고양이 이미지 ➡️ 우는 고양이 이미지)
  • Task2도 image함수를 실행하고 아직 cache가 비어 있기 때문에 await 키워드까지 함수가 진행되고 다운로드가 완료되는 동안 suspend된다.
  • Task1이 작업을 마치고 cache 딕셔너리에 웃는 고양이 사진을 저장한다.
  • Task2도 작업을 마치고 cahe 딕셔너리에 우는 고양이 사진을 저장한다
  • 즉, 캐시가 이미 이미지로 채워 졌음에도 Task1은 웃는 고양이, Task2는 우는 고양이 사진을 리턴한다. (서로 다른 이미지 리턴)
  • 이미지를 캐싱하면 항상 동일한 URL에 대한 동일한 이미지를 반환하므로 적어도 캐시에서 수동으로 제거할 때까지는 사용자 인터페이스가 일관성을 유지할 수 있다.
  • 그러나, 이번 예시에서는 이미지가 예기치 않게 변경되었다.
  • 우리는 Actor를 사용해 low-level의 Data race를 방지했지만, await를 가로질러 상태에 대한 가정을 했기 대문에 잠재적인 버그를 가지게 되었다.
  • 해결책은 await 이후에 우리의 가정을 확인하는 것이다.

  • await 이후에 캐시를 기록할 때 이미 항목이 있으면 원래 버전을 유지하고 새 버전은 버리는 방식을 취하는 것이다.
  • 더 나은 해결책은 중복 다운로드 자체를 완전히 방지하는 것이다.

 

Actor reentrancy

  • Actor reentrancy는 Deadlock을 방지하고 forward progress를 보장하지만 각 await 지점에서 assumption(가정)을 확인해야 한다.
  • Reentrancy를 잘 설계하려면 동기 코드 내에서 Actor 상태의 mutation을 수행해야 한다.
  • 모든 상태 변경이 잘 캡슐화되도록 동기화 기능 내에서 수행하는 것이 이상적이다.
  • 상태 변화는 일시적으로 Actor를 일관성 없는 상태로 만드는 것을 수반할 수 있다.
  • await 전에 일관성을 복원(restore)해야 한다.
  • 그리고 await가 잠재적인 suspension point라는 것을 기억하자!!!!
  • 코드가 일시 중단되면 다시 시작되기 전에 프로그램과 세계가 바뀌게 된다. (상태 변화 발생 가능)
  • Global state, clocks, timers, Actor에 대한 가정은 await 후에 확인해야 한다.

 


 

Actor isolation

Actor isolation은 Actor 타입 행동의 기본이다.

Actor isolation이 프로토콜 준수, 클로저, 클래스를 포함한 다른 언어 기능과 어떻게 상호 작용하는지 알아보자!

 

Protocol

  • 다른 타입과 마찬가지로 Actor는 프로토콜을 채택할 수 있다.
  • 위의 예시에서는 Equatable 프로토콜을 준수하도록 했다.
  • idNumber를 기준으로 두 LibraryAccount를 비교한다.
  • 메서드가 static이기 때문에 self 인스턴스가 없으므로 actor에게 isoloated 되지 않는다. (not isolated)
  • 대신 2개의 actor 타입 매개 변수를 가지고 있는데(lhs, rhs), 이 정적 메서드는 두 매개변수로부터 모두 밖에 있다.
    • Actor의 immutable 상태에만 액세스하기 때문에 문제 없다! (let idNumber)

  • Hashable 프로토콜을 채택시키면 어떨까?
  • hash 함수를 구현해야 하지만 컴파일 에러가 발생한다.
  • Hashable을 채택한다는 것은 이 hash 함수가 Actor 외부에서 호출될 수 있다는 것을 의미하지만, hash(into:)는 비동기적이지 않기 때문에 actor isolation을 유지할 방법이 없다.

  • 메서드에 nonisolated 키워드를 붙이면 이 문제가 해결된다.
  • Nonisolated는 이 메서드가 actor에 선언되어 있음에도 불구하고 Actor 외부에 있는 것으로 취급된다는 것을 의미한다.
    • 즉, Actor 내부 함수에서 자동으로 작동하는 동기화 메커니즘이 nonisolated 함수에는 작동하지 않는다.
  • 이는 Hashable 프로토콜의 동기 요구 사항을 충족할 수 있음을 의미한다.
  • nonisolated 메서드는 Actor 외부에 있는 것으로 취급되기 때문에 Actor에서 가변 상태를 참조할 수 없다.
    • 위 예시에서는 let으로 선언한 idNumber로 해싱하기 때문에 문제가 없지만 만약 booksOnLoan 속성으로 해싱하려고 하면 컴파일 에러가 발생하게 된다.

 

 


정리 ⭐️

동기, 비동기, isolation, 동기화 메커니즘 등... 다양한 용어가 반복적으로 나와서 헷갈림을 유발합니다.

이 시점에서 한번 개인적으로 이해한 바를 정리해보겠습니다. (틀린 내용이 있다면 댓글로 피드백 해주세요!!)

  • 기본적으로 Actor는 동시성 프로그래밍에서 Data race 없이 공유 가변 자원을 제공합니다.
  • 즉, 내부적으로 동기화 메커니즘이 동작하여 Actor에 선언된 함수들은 기본적으로 여러 스레드가 동시에 접근해도 한 스레드씩만 해당 함수를 실행할 수 있습니다.
  • Atomic하게 함수가 동작하는 것이죠! (자체적으로 mutex가 걸려있는 것처럼 동작한다고 이해했습니다.)
  • 중요한건!!! Actor 외부에서 Actor에 접근할 때는 비동기적으로 수행해야 한다는 것입니다!!!
  • Actor는 동기화 메커니즘인데 왜 이번에는 또 비동기라는거지..? 라고 할 수 있지만 제가 생각한 이유는 다음과 같습니다.
    1. Actor의 내부 요소들에는 동기화 메커니즘 작동 (actor isolation == 동기화 메커니즘 작동으로 이해했습니다.)
    2. 만약 Actor에 있는 increment()라는 함수를 외부의 3곳에서 동시에 호출하면 1개의 task 씩만 increment() 함수 실행 가능
    3. 즉, 호출하는 입장에서 생각해보면 counter.increment()를 호출했지만 이것이 즉시 실행되는 것이 아니라 내 차례가 올 때까지 기다려야 하는 상황 발생
    4. 호출하는 입장에서는 내가 실행한 코드가 언제 끝나는지 보장 불가 ➡️ 이 것은 비동기의 특성!!!
    5. 결과적으로 Actor 내부는 동기적으로 동작하지만 외부의 입장에서 보면 Actor의 메서드들은 비동기적으로 호출해야 하는 것입니다.

실제로 actor Counter의 increment 함수에는 async 키워드가 없지만 await 키워드를 붙여야 increment() 함수를 실행할 수 있습니다.

안 붙이면 위처럼 컴파일 에러가 발생합니다.

 

만약 actor 메서드에 nonisolated 키워드를 붙이면 해당 메서드가 Actor 외부로 인식하기 때문에 더이상 동기화 메커니즘의 장점은 누릴 수 없지만 더이상 외부에서 호출할 때 비동기로 호출하지 않아도 됩니다. (await 키워드 필요 없어짐)

테스트 해봤을 때 nonisolated를 붙이면 await 없이 counter.increment()를 실행할 수 있게 되었습니다.

하지만, 이제 격리되지 않은 환경이기 때문에 increment 메서드 내부에서 isolated 속성인 value에 접근 하는 것에 대해 컴파일 에러가 발생하게 됩니다.

당연하게도 이렇게 let으로 선언한 속성에 대해서는 가변 상태가 아니기 때문에 nonisolated 메서드에서 접근하는것이 에러 없이 가능했습니다.


Closures

  • 클로저는 한 함수 내에서 정의된 작은 함수이며 이후 다른 함수로 전달되어 나중에 호출될 수 있다.
  • 함수와 마찬가지로 클로저는 actor-isolated 되거나 nonisolated일 수 있다.
  • 위 예제에서는 대출 중인 각 책에서 일부를 읽고, 읽은 총 페이지 수를 리턴한다.
  • reduce 호출은 읽기를 수행할 클로저를 포함한다.
  • readSome의 호출에는 await 키워드가 없다.
  • 왜나하면 이 클로저는 actor-isolated 함수인 read 함수 내에 형성되어 있고 이 클로저 자체가 actor-isolated 되기 때문이다.
  • reduce 작업이 동기적으로 실행되므로 이 작업은 안전하며 동시 액세스를 유발하는 스레드로 부터 클로저를 escape 할 수 없다. (escaping 클로저는 함수 스택 프레임을 벗어나서 클로저를 실행할 수 있는데 여기서의 클로저는 그러한 특성이 없다는 의미인 듯)

  • 하지만 만약 나중에 읽는 함수 구현을 위해 detached task에서 read를 수행하면 어떻게 될까?
  • Detached Task는 Concurrently하게 클로저를 실행한다. (액터가 실행 중인 작업과 동시에 수행)
  • 따라서, 이 클로저는 actor 에서 실행되지 않거나 Data race가 발생할 수 있다.
  • 즉, 이 클로저는 not-isolated이다.
  • read 함수를 호출하려면 위와 같이 비동기적으로 호출해야 한다. (await 키워드를 붙여야 한다는 의미)

 

Actor isolation and Data

  • actor 타입인 LibraryAccount가 가지는 Book이 Struct라고 해보자!
  • 이것은 좋은 선택이다.
    • LibraryAccount actor의 인스턴스의 모든 상태가 독립적이라는 것을 의미 (값타입의 장점!)
  • selectRandomBook 메서드를 호출하면 actor가 가진 책중에서 하나의 책의 복사본을 얻을 수 있다.
  • 우리가 이 복사본을 변경해도 원본에는 영향을 미치지 않는다!

  • 하지만! Book을 Class로 바꾼다면 상황이 바뀐다.
  • 이제 LibraryAccount actor는 Book class의 인스턴스를 참조한다. (refrence 타입의 특징)
  • 이것 자체는 문제가 아니다.
  • 그러나 selectRandomBook 메서드를 호출하면 자신이 가지고 있는 Book 중에 하나의 책의 주소값을 넘기게 된다.
  • 이제 우리는 actor 외부에서 actor의 mutable state에 대한 참조를 가지게 되었다.
  • 즉, Data race에 대한 가능성이 생겨버린 것이다.
  • selectRandomBook을 통해 얻은 book의 제목을 수정하면 actor가 소유한 원본 book의 이름이 바뀌게 된다.
  • visit 함수가 actor 외부에 있기 때문에 동기화 메커니즘이 동작하지 않을 것이고 이러한 책 이름 변경은 Data race가 될 수 있다.

 

정리하자면, Value type과 Actor는 Concurrently하게 사용해도 안전하지만 class는 문제가 발생한다. 이렇게 concurrently 하게 사용해도 안전한 타입들을 지칭하는 이름이 있는데 바로 Sendable이다.

 

Sendable ⭐️

  • Sendable 타입은 값이 다른 actor들 간에 공유될 수 있는 타입이다.
  • 한 위치에서 다른 위치로 값을 복사하고 두 위치 모두 해당 값의 복사본을 서로 간섭하지 않고 안전하게 수정할 수 있는 경우 해당 타입은 Sendable이 될 수 있다.
  • 값 타입은 앞선 예시들처럼 각 복사본이 독립적이기 때문에 Sendable이다.
  • Actor 타입은 가변 상태에 대한 액세스를 동기화하므로 Sendable이다.
  • Class는 주의 깊게 구현된 경우에만 Sendable일 수 있다.
    • 예를 들어, class와 모든 subclass에 immutable data만 있는 경우 Sendable이라고 할 수 있다.
    • 또는, class가 안전한 동시 액세스를 보장하기 위해 lock과 같은 내부적으로 Synchronization을 수행하는 경우 Sentable 일 수 있다.
  • 하지만 대부분의 Class는 이렇게 구성하지 않기 때문에 Sendable하지 않다.
  • 함수는 반드시 Sendable 할 필요가 없기 때문에 actor를 통과하는 것이 안전한 새로운 종류의 함수가 있다.
    • @Sendable function

 

  • 모든 동시 코드를 사용하는 actor는 기본적으로 Sendable 타입으로 통신해야 한다.
  • Sendable 타입은 Data race로 부터 코드를 보호한다.
  • Swift가 정적으로 Sendable인지 체크한다. (컴파일 시점에 체크)
  • Sendable은 프로토콜이며 다른 프로토콜들처럼 우리가 만든 타입에 채택시킬 수 있다.
  • 위의 사진은 Book 구조체에 Sendable을 채택시켰다.
  • 저장된 모든 속성이 Sendable 타입인 경우 Book 또한 Sendable일 수 있다.
  • 여기서는 Book이 소유한 Author 타입이 Class이고 이는 Author 배열이 Sendable하지 않다는 것을 의미한다.
  • 따라서 Swift는 Book이 Sendable하지 않다고 컴파일 에러를 발생시킨다.

 

  • 제네릭 타입의 경우 Sendable여부는 Generic Argument에 따라 달라질 수 있다.
  • Conditional conformance를 사용하여 적절한 때에 Sendable을 전파할 수 있다.
    • 위의 예시처럼 where문을 활용하여 T, U 모두 Sendable 일 때 Pair 또한 Sendable을 준수하게 한다.

 

@Sendable functions

  • 함수 자체가 Sendable 일 수 있으며 이는 function value를 Actor 간에 전달 하는 것이 안전하다는 것을 의미한다.
  • 이는 Data race를 방지하기 위해 클로저가 수행할 수 있는 작업을 제한하는 클로저의 경우 특히 중요하다.
    1. Sendable 클로저는 로컬 변수에서 data race가 발생할 수 있기 때문에 가변 로컬 변수를 캡처할 수 없다.
    2. 클로저가 캡처하는 모든 것들은 Sendable 해야 한다. actor 바운더리를 넘어 non-sendable 타입을 이동시킬 수 없다.
    3. 동기 Sendable 클로저는 외부에서 actor 외부에서 코드를 실행할 수 있기 때문에 actor-isolated가 될 수 없다.

 

Sendable 클로저의 사용 예시

  • Task의 detached 메서드는 파라미터로 @Sendable 클로저를 사용한다.
  • 이 세션의 초반에 사용한 예제이고 increment 함수를 동시성으로 실행하려고 했기 때문에 컴파일 에러가 발생했었다.
  • detached 함수가 @Sendable 클로저를 받아야 하기 때문에 Swift가 에러를 발생시킨 것이다.
    • Sendable 클로저는 로컬 변수에서 data race가 발생할 수 있기 때문에 가변 로컬 변수를 캡처할 수 없기 때문!

  • Detached Task의 클로저는 Sendable이므로 이 클로저와의 상호 작용은 비동기적이어야 한다.
  • Sendable 타입과 클로저는 가변 상태가 여러 actor 간에 공유되지 않으며 동시에 수정할 수 없음을 확인하여 Actor isolation을 유지하는데 도움이 된다.

 

Main Actor

  • 메인 스레드는 사용자 인터페이스 렌더링과 사용자 상호 작용 이벤트가 처리되는 곳이다.
  • UI 작업은 메인 스레드에서 수행해야 한다.
  • 그러나 메인 스레드에서 모든 작업을 수행하고 싶지는 않다.
    • 느린 입출력 작업이나 서버와의 통신 등 메인 스레드에서 작업을 너무 많이 하면 UI가 freeze되는 문제가 생긴다.
  • 따라서, UI와 상호 작용할 때는 메인 스레드에서 작업해야 하지만 비용이 많이 들거나 대기 시간이 오래 걸리는 작업을 위해서는 메인 스레드에서 벗어나야 한다.

 

  • 따라서 지금까지 윌는 메인 스레드에서 실행되어야 하는 작업이 있을 때마다 코드에 DispatchQueue.main.async를 호출했었다.
  • 이 코드의 구조는 모호하게 익숙하다!
  • 사실, 메인 스레드와 상호작용 하는 것은 actor와 상호작용하는 것과 매우 비슷하다.
  • 메인 스레드에서 이미 실행중인 것을 알고 있는 경우 안전하게 액세스하여 UI 상태를 업데이트 할 수 있다.
  • 메인 스레드에서 실행하지 않는 경우, 이 스레드와 비동기적으로 상호 작용해야 한다.
  • 결국 이것은 Actor들이 동작하는 방식과 같다.
  • ➡️ Main Thread를 describe하는 Main Actor가 존재한다.

 

  • MainActor는 메인 스레드를 대표하는 Actor이다.
  • 이것은 2가지 중요한 점에서 일반 Actor와 다르다.
    1. MainActor는 모든 동기화를 Main Dispatch Queue를 통해 수행한다.
      • 이는 런타임 관점에서 MainActor가 DispatchQueue.main을 사용하는 것과 호환된다는 것을 의미한다.
    2. 메인 스레드에 있어야 하는 코드와 데이터가 곳곳에 흩어져 있다.
      • SwiftUI, AppKit, UIKit 및 기타 시스템 프레임워크에 있다.
      • 사용자 자신의 뷰, 뷰 컨트롤러 및 데이터 모델의 UI 대면 부분에 펼쳐져있다.
  • Swift Concurrency를 사용하면 MainActor Attribute로 선언을 표시하여 MainActor에서 실행해야 함을 알릴 수 있다. (@MainActor로 표기)
  • MainActor 외부에서 호출하는 경우 await를 적어야 하며 이 호출은 비동기적으로 메인 스레드에서 수행된다.
  • 메인 스레드에서 실행되어야 하는 코드를 MainActor로 표시함으로써 DispathQueue.main 사용 시기에 대한 추측이 더 이상 없어진다.
  • Swift는 위의 코드가 항상 메인 스레드에서 실행되도록 한다.

  • 타입에도 MainActor를 배치할 수 있으며 모든 멤버와 하위 클래스에 MainActor가 배치된다. (@MainActor class로 선언 가능)
  • 이는 대부분의 모든 것이 메인 스레드에서 실행되어야 하는 UI와 상호 작용해야 하는 코드 기반의 부분에 유용하다.
  • 개발 메서드는 일반 Actor처럼 익숙한 방식으로 nonisolated 키워드를 사용하여 제외시킬 수 있다. (이 키워드가 붙은 함수들은 @MainActor에서 동작 X)
  • UI 대면 타입 및 작업에 MainActor를 사용하고 다른 프로그램 상태를 관리하는 데 사용자 고유의 Actor를 도입하여 안전하고 올바른 Concurrency 사용을 보장하는 앱을 설계할 수 있다.

 

 

정리

 

 

 

'iOS > Swift' 카테고리의 다른 글

Beyond the basic of structured concurrency  (4) 2023.11.09
Swift concurrency: Behind the scenes  (0) 2023.09.13
Swift의 분산된 Actor 소개  (0) 2023.08.22
Adopting Swift Packages in Xcode  (0) 2023.06.14
Creating Swift Packages  (1) 2023.06.07

댓글