WWDC) Protect mutable state with Swift Actors

Havi·2021년 6월 10일
0

WWDC

목록 보기
7/7

wwdc 주소
se-0306

Concurrent Program을 작성하는데 있어서 fundamental한 문제는 Data race condition이다.
데이터 경쟁은

  • 두개의 쓰레드가 동시에 같은 데이터에 접근할 때
  • 그 중 하나가 write일 때
    발생한다.
class Counter {
    var value = 0
    
    func increment() -> Int {
    	value += 1
        return value
    }
}

let counter = Counter()

asyncDetached {
    print(counter.increment()) // 1 or 2
}

asyncDetached {
    print(counter.increment()) // 2 or 1
}

위와 같은 상황에서 print(counter.increment()) 는 동시에 같은 값을 읽어왔을 때 (1, 1), (2,2)를 가질 수 있다.

이러한 문제는 프로그램의 각각 다른 부분에서 작동할 수 있기 때문에 디버그 하기 힘들다.

이러한 데이터 레이스는 shared mutable state에 의해 야기된다.

이러한 문제를 해결하려면 value semantics를 사용해서 shared mutable state를 없애면 된다.

위 counter 코드를 value semantics로 바꾸면 다음과 같이 되지만 컴파일러 에러가 생긴다.

따라서 counter를 var로 바꿔주면 unsafe code 에러가 뜬다.

그래서 로컬 변수로 잡아주면 계속 1이 나오게 된다.

그래서 race free하지만 우리가 원하는 동작이 나오지 않게된다.

그래서 다음과 같은 primitives가 존재한다.

하지만 이 아이들을 정확하게 사용하려면 매번 섬세한 규율을 필요로 하는 약점을 가지고 있다. 그렇지 않으면 경쟁상태에 빠지게 된다.

Actors

따라서 Actor가 등장한다. Actor는 shared mutable state를 위한 synchronization을 제공하고, 다른 프로그램에서부터 isolated된 자신만의 state를 가진다.

  • 이 State에 접근하기 위해서는 actor을 통해서만 가야한다.
  • actor는 mutually-exclusive access를 보장한다.
  • structs, enums, classes와 비슷한 capabilities를 가진다.(property, extension ...)
  • Reference Type이다.
  • instance의 data를 isolated된 공간에 저장하여 synchronization을 보장한다.

따라서 위의 카운터 코드를 actor로 바꾸면 1,2 또는 2,1를 보장받을 수 있다.

아래 함수는 Counter의 데이터에 직접 접근하고 synchronous하게 increment()를 실행할 수 있다. actor안에서 실행되기 때문에 await키워드가 필요하지 않다.

Actor reentrancy

다음 코드는 동일한 이미지를 여러번 다운받지 않기 위해 cache에 이미지를 저장하는 예제이다. 이 코드는 actor안에서 실행되기 때문에 concurrent하게 이미지가 다운로드 되더라도 cache에는 1개의 task만 접근함이 보장된다.

await 키워드를 사용하며 주의할 점은 함수가 해당 시점에 suspended될 수 있다는 것이다. 즉 cpu가 다른 우선순위가 높은 task를 먼저 실행할 수 있다.

다음과 같이 동시에 2개의 Task가 동일한 URL로 실행되고, Task1이 이미지를 다운받는동안 서버의 이미지가 바뀌었을 때, Task1이 먼저 resume된 뒤 cache된다.

다음 Task가 resume되면 캐시된 이미지가 바뀐다.

따라서 예상치 못하게 캐시된 이미지가 바뀔 수 있다.

그래서 우리는 값이 없으면 default로 다운로드된 이미지를 넣고, 값이 있으면 캐시에 존재하는 이미지를 넣어준다.

actor reentrancy는 deadlock을 예방하고, forward progress를 보장한다.

(이 부분은 정확하게 이해하지 못함 ... )

Actor isolation

actor isolation이 protocol conformances, class, closures와 어떻게 interact하는지 알아보겠다.

Equatable을 준수하게 했을 때 static함수이기 때문에 self가 없고, immutable하기 때문에 괜찮다.

Hashable을 적용할 경우 compile 에러가 난다. 왜냐하면 actor 밖에서 이 함수가 실행될 수 있기 때문이다. 하지만 hash(into:)는 async하지 않고, actor isolation을 유지할 수 없다. 따라서 nonisolated 키워드를 붙여주어야한다.

nonisolated 키워드는 actor 안에 정의되어 있더라도 actor 밖에 있는것 처럼 취급한다. 따라서 actor의 muttable state를 reference할 수 없다. 하지만 이 함수는 immutable한 idNumber에 접근하기 때문에 괜찮다. booksOnLoan에 접근하면 complie에러가 난다.

위 함수에서 reduce는 synchronously하게 실행되므로 await키워드가 필요하지 않다. 따라서 closure를 escape해서 다른 쓰레드에서 concurrent access를 야기하지 않는다.

asyncDetached함수는 closure를 concurrent하게 다른 work와 실행한다. 따라서 closure는 actor에 있을 수 없으므로 isolated되어있지 않기 때문에 await 키워드를 사용해야 한다.

위와 같이 actor안에 value타입의 instance가 저장될 때, actor밖에서 data를 passing하는 것은 괜찮다. 하지만 Book이 class타입이라면 복잡해진다.

이렇게 Book의 instance들을 refernece하고 있게 될 경우, actor 안에서는 괜찮다.

하지만 actor밖에서 데이터를 전달할 경우 visit메소드가 actor안에 없기 때문에 데이터 레이스를 야기할 수 있다.

value type은 concurrent하게 써도 괜찮지만 class는 안된다. 따라서 Sendable이 필요하다.

Sendable

Sendable은 여러 개의 actor에서 값이 share될 수 있는 타입이다. value타입과, actor타입은 Sendable이다. class의 경우 자신과 모든 subclass의 프로퍼티가 immutable하거나, internally-synchronized class인 경우 가능하다. 하지만 대부분의 클래스는 그렇지 않으므로 Sendable할 수 없다. 함수의 경우 @Sendable을 붙여준다.

Sendable 타입은 데이터 레이스를 막아주고, static하게 체크된다.
Sendable 프로토콜을 준수할 경우 가능한지 체크해주는데 Author가 클래스 타입이어서 에러가 날 수 있다.

제네릭일 경우 Sendable을 argument가 준수할 경우에 가능하다.

Sendable 함수는 immutable local variable을 capture할 수 없다. 캡쳐할 수 있는 것은 sendable type이다.

위에서 본 예제는 @Sendable한 closure가 동시에 local variable에 접근하려 하기 때문에 에러가 나온다.

Main Actor

메인쓰레드에서 동작을 보장하게 하는 DispatchQueue.main.async { }와 같이 Main Tthread에서 동작을 보장하는 Actor가 Main Actor이다.

Main Actor는 synchronization을 main dispatch queue에서 한다. 이는 runtime 관점에서 dispatchqueue.main으로 interchangable하다.

Main Actor에서 실행되어야 할 데이터는 모든 곳에 흩어져 있다. 따라서 @MainActor키워드를 붙여준다. MainActor 밖에서 실행될 경우 await해야한다.

타입도 MainActor에서 실행될 수 있다. 메인 액터 클래스의 모든 메소드와 프로퍼티는 MainActor에서 실행된다. UI에서 실행되는 class에서 유용하다.

profile
iOS Developer

0개의 댓글