본문 바로가기

iOS/Concurrency

[Swift 5.5] actor 개념 뿌수기!! +_+ #Concurrency, thread-safe, actor's serial executor | No1. Actor

 

안녕하세요.

 

이번 포스트는 Swift 5.5 concurrency api 중 actor에 대해서 공부한 개념을 정리하려고 합니다. Actor를 공부하기 전에 같이 공부하면 좋을 @Sendable, Sendable protocol에 관한 글 또한 정리 했습니다. 참고해주시면 감사합니다.

 

시작하기에 앞서 thread-safe라는 개념에 대해 알고 가면 좋습니다.

 

1. What does "thread-safe" mean?

Main thread에서 호출하여 수행되나 동시에 background thread에서 호출되어 수행되나 같은 결과를, 예상되는 결과를 갖는 경우를 의미합니다. 여러 thread에서 공유 자원(object, method etc..)을 같은 시간 대에 동시에(concurrently) 수행해야 하는 상황에서 같은 결과가 주어집니다. A B C라는 thread(근로자)가 누가 먼저 한 번씩 사용하든 동시에 사용하던 순서 상관 없이 프로그램 실행에 문제가 없는 것을 의미합니다.

 

Cpu칩이 multi core로 늘어남에 따라 공유 자원을 여러 thread가 동시에 사용할 수 있게 됬는데 생산성이 높아지는 만큼 concurrent problem이 발생할 수 있습니다. 대표적으로 data race, deadlock등이 있습니다. 

 

Concurrent problem: Data race

Data race는 예측할 수 없는 결과를 불러옵니다. 메모리 손실, 불안정한 테스트, 이상한 crash etc...

 

학교 수업시간에 배운 내용을 떠올려 Data race를 예로 들자면.. 수박이의 은행 잔고에는 10,000원이 있습니다. 그리고 사과가 수박이한테 천원을 입급하려고 합니다. 동시에 포도도 수박이한테 천원을 입금하려 합니다.

 

  사과 포도
  atm 기기 1 atm 기기 2
수박이 은행 잔고 10,000 10,000

 

(계좌에 돈 얼마인지 볼 수 있다는 가정하에)같은 시간에 사과의 입장에서 수박이 은행 잔고에는 만원이 있다는 것을 확인했고 포도 또한 수박이 은행 잔고에 만원이 있다는 것을 확인했습니다.

 

둘 다 동시에 수박이 은행 잔고에 접근했기 때문에 수박이 은행 잔고는 10,000이 있다는 것을 둘 다 인지했습니다.

동시에 연산을 수행한다면,,

 

수박이 은행 잔고 = 사과(입금 1,000) + 수박이 은행잔고(사과가 봤을 때 10,000)

수박이 은행 잔고 = 포도(입금 1,000) + 수박이 은행잔고(포도가 봤을 때 10,000)

 

결국  수박이 은행 잔고에는 11,000원만 들어가 있게 된다는 놀라운 사실이... read, write가 동시에 사용됬기 일어난 것 입니다. 이를 data race라고 합니다. 

 

 

마찬가지로

class Plus {
    private var value = 0
    func increment() { value += 1 }
}

 

흔히 사용하는 코드 대부분이 사실 not thread-safe일 수 있습니다. 위 경우 또한 not thread-safe 코드입니다. 단순히 변수를 private로 선언하고 변수의 수정은 함수를 통해서만 하도록 코드를 구성하는 경우는 thread-safe를 보장할 수 없습니다. 위에서 말한 예시와 동일합니다.

 

value = value + 1을 하는 시점에, 두 개 이상의 thread에서 동시에 수행하는 경우. 각각의 thread에서 메모리에서 가져온 value 값이 1임을 확인하고 1을 더하는 연산을 모두 실행하게 된다면 결과적으로는 2가 저장될 수 있습니다(or crash). 위 코드는 main thread에서만 사용될 경우 문제가 없지만 동시성이 요구되는 호출에서 결과적으로 메모리에 불안정한 값이 발생할 수 있다는 것입니다. (단순 접근이 아니라 특정 메모리에 한 개 이상의 read write가 동시에 있을 때)

 

이 코드가 thread safe가 되기 위해선 NSLock, dispatch barrier 세마포어, synchronization한 연산을 수행하는 등의 상호 배제 알고리즘을 적용해야 합니다.

 

정말 깨알 요약 하자면, value 라는 shared mutable state 가 concurrent access(read,write etc...)가 요구될 때, 사전 예방조치를 취하지 않는 모든 코드는 not-thread safe라고 바라볼 수 있습니다.

 

 

Swift 5.5 이전에는 특정 메서드의 연산이 thread-safe를 보장하는지 확인하려면 공식 문서에서만 확인할 수 있었습니다. 그리고 concurrent programming에 있어 concurrent problem을 방지하기 위해서 위와 같은 알고리즘을 적용해야 했습니다. 코드를 깜빡하고 임계 영역 해제하는 코드를 작성하지 않을 수도 있습니다. 물론 컴파일러는 코드를 빼먹었다는 경고 문구도 안 알려줍니다. 이런 문제와 concurrent problem을 보완하고자 Swift 5.5 concurrenct api에서 actor라는 새로운 타입이 등장했습니다.

 

2. Hi, "actor"

위에서 언급된 actor는 data race와 같은 concurrent problem에 대해 컴파일러가 사전에 감지하고 경고를 전달함으로 data race등을 예방할 수 있습니다. struct, enum, class 등의 타입 처럼 새로운 타입으로 디자인 되었습니다. Class와 같은 reference type입니다. 그렇다면 "class와 다른점은 무엇인가?"를 살펴보기 이전에, actor를 사용하는 경우에 대해서 알아야 합니다.

 

Actor는 concurrent domains(multi threads)간에 shared mutable stateaccess(call,read,write ...)할 때 발생할 수 있는 concurrent problem을 런타임이 아닌 컴파일 단에서 컴파일러가 체크할 수 있도록 해주는 유용한 객체입니다. Concurrent problem을 방지하는 actor는 thread-safe라 할 수 있습니다. 

 

컴파일러는 actor 내부 멤버가 data race가 발생할 수 있는 코드를 작성했을 때, 경고를 띄울 수 있는 이유는 actor만의 특별한 조건이 있기 때문입니다. actor의 내부 멤버는 state isolation layer로 분류 됩니다. 각각의 멤버는 isolated state를 띄게 됩니다.(class, struct등 일반적인 멤버와 상태를 구분하기 위해서) 그리고 또 한 가지, actor는 Sendable protocol을 준수합니다. (관련 개념 정리 글)

 

actor's serial executor

위의 기본 조건이 있고 추가로 actor's serial executor 때문에 각각의 isolated state에 대한 safety access와 mutate가 가능합니다. Actor 내부에 수행되야 할 코드들은 actor's serial executor에 의해 synchronization 처리가 됩니다. GCD의 serial queue와 유사한 동작을 수행한다고 생각하면 편합니다.

 

특징은 serial 답게 한 thread에서 특정 시간대에 특정한 task 한 개만 해당 thread에서 실행된다는 점입니다. 런타임 때 actor's member를 호출, 관리합니다(serial하게). 추가적으로 Sendable을 준수한다는 개념 덕분에 내부 멤버들은 Sendable을 준수해야 합니다. Sendable을 준수할 경우 Sendable의 규칙에 의해 cross-actor, concurrency domains간 value access가 안전하다는 보장이 됩니다. 즉 동시적인 접근에서 thread-safe하다는 보장입니다.

 

Multi threads가 동시에 특정 state를 접근해도 해당 isolated state를 갖는 actor's serial executor에 의해 synchronization하게 수행되기 때문입니다. 이와 반면 클래스는 concurrent domains간 multi thread가 특정 state(프로퍼티, 인스턴스 등등)에 접근할 때 동시적(Concurrently)으로 access가 되기 때문에!!! concurrent problem이 발생하는 것입니다.

 

Actor는 actor 내부 멤버를 isolated state로 특별하게, 소중히 간주하고 actor 내부에 존재하는 isolated-state를 해당 actor가 아닌 외부에서 접근할 때 non-isolated state로 간주하며(내 주체로 다루지 않는 state는 non-isolated state!!) error를 발생시킵니다.

 

actor 잠깐 요약

  • actor는 다른 object와 다르게 내부 멤버(프로퍼티, 함수 등) isolated state를 부여하고 serial executor를 통해 스케줄링된다.
  • actor-isolated state 덕분에 self 범위 내에서 자유롭게 접근이 가능한 반면 self이외의 영역은 non actor-isolated로 간주한다.(물론 state가 immutable인지 mutable인지에 따라 다릅니다.)
  • actor's serial executor는 concurrent가 아닌, serial한 수행을 한다.

 

다시 위의 코드에서 Plus 타입을 class 에서 actor로 바꾸게 된다면 shared mutable state가 thread safe함이 보장됩니다. 자기 자신 내 mutable state인 actor-isolacted는 내부에서 자유롭게 값을 변경할 수 있습니다.

 

특정 actor self 영역의 isolated state를 다른 actor에서 변경하려 access하려 할 경우 cross-access reference라고 합니다. 이 경우에 actor's isolated state가 immutable 한 경우 (ex. value type) 어디서 사용하던 문제가 없습니다. read-only일 테니까요. 하지만 mutable인 경우에는 얘기가 달라집니다.

이 경우나 다른 외부에서 특정 isolated state를 변경하려 할 때는 async하게 특정 actor's serial executor한테 부탁해야 합니다. 그렇지 않을 경우 누구나 다 호출과 동시에 수행하게 될 경우 class와 다를게 없기 때문에 data race될 수 있기 때문입니다.

 

지금까지 Actor의 특징에 대해 간략하게 정리했습니다. 다음 포스트는 actor, actor-isolated, cross-actor, MainActor의 개념에 대한 주제입니다.

 

[Swift 5.5] Actor No2. actor, actor isolation, cross-actor reference 개념 완벽 뿌수기 !!! | Concurrency

안녕하세요. 저번 포스트에선 actor의 개념과 thread-safe, actor's serial executor에 대해 공부했던 개념을 정리했습니다. 이번 포스트는 actor와 isolated state, Sendable(관련 개념 정리)을 준수하며 actor를 사용

dev-with-precious-dreams.tistory.com

 

긴 글 읽어주셔서 감사합니다. 틀린 부분 발견시 댓글로 알려주신다면 정말 감사합니다.

 

Swift 5.5 다른 포스트

GCD의 문제, Concurrency model 특징과 async/await 개념 정리 포스트

Actor No2. actor, actor isolation, cross-actor reference 개념 정리 포스트

Sendable protocol 개념 정리 포스트

Structured concurrency 개념 정리 포스트

References

https://www.kodeco.com/books/modern-concurrency-in-swift

 

Modern Concurrency in Swift

Master Swift’s modern concurrency model! For years, writing powerful and safe concurrent apps with Swift could easily turn into a daunting task, full of race conditions and unexplained crashes hidden in a massive nesting of callback closures. In Swift 5.

www.kodeco.com