[Swift] Actor(1)

2024. 10. 21. 18:40·Swift

오늘은 WWDC21 - Protect mutable state with Swift actors를 보며 Actor에 대해 파헤쳐 볼 것이다 !
아직 Concurrency도 잘 못다루는 나이기에 .. 조금 두렵지만 ... 레쭈고 ~

Actor의 탄생 배경

Data race는 동시성 프로그래밍을 하는데 가장 신경쓰기 어려운 오류 중 하나이다. Data race는 보통 두 개의 서로다른 스레드가 동시에 mutable한 데이터에 접근하는 과정에서 발생한다.

이러한 Data race는 발생하기는 꽤나 쉽지만 개발자가 디버깅을 하기에는 굉장히 까다로운데, 보통 Data race를 유발하는 데이터 접근이 프로그램의 서로 다른 부분에서 이루어질 확률이 높고 이 때문에 개발자가 디버깅을 하기 위해서는 비지역적 추론이 필요하기 때문이다.

Data race를 피할 수 있는 방법에는 값 타입을 사용하여 공유 가능한 mutable state를 아예 제거해버리는 방법이 있다. 특히나 값 타입에서 let 프로퍼티를 사용하면 완전히 immutable하기 때문에 Data race가 발생할 일은 원천봉쇄가 된다. 이렇게 값 타입을 사용하면 프로그램의 추론이 더 쉬워지고 동시성 프로그래밍을 하면서도 더욱 안전하게 사용할 수 있기 때문에 실제로도 Swift에서는 값 타입의 사용을 권장하고 있다.

위와 같은 참조 타입의 Counter가 있을 때, 두 개의 각기 다른 Task에서 increment 함수를 호출하게 된면 Data race가 발생하게 된다. 하지만 컴파일러는 이를 감지할 수 없기에 디버깅이 어려워진다.

그럼 Counter를 값 타입으로 바꾸면 어떻게 될까?

값 타입으로 바꾸면 바로 컴파일러가 에러를 내뱉게된다. 값 타입에는 한 번에 하나의 스레드만 접근할 수 있는데 서로 다른 Task 내부에서 동시에 접근할 수 있다는 것을 컴파일러가 예측 가능하기 때문이다.

우리는 위 에러를 해결하기 위해 아래 사진과 같이 Task 내부에 counter 객체를 지역변수로 하나 생성해 줄 수 있다.

이 코드는 이제 Data race가 일어나지 않는 코드가 되었다. 하지만 우리가 원하는 동작(하나의 Counter 객체의 value 프로퍼티의 값을 증가시키는 동작)을 하지는 못한다. 결국 우리는 공유 가능한 mutable state가 필요한 것이다.

공유 가능한 mutable state를 동시성 프로그래밍에서 사용하기 위해서는 동기화 과정이 필요하다. 기존에 있는 원시적인 동기화 방법은 low-level tool인 Atomics나 Locks, high-level constructs인 Serial disptach queue가 있다. 하지만 이 모든 것들은 매번 정확하고 신중하게 사용해야하며 개발자가 제대로 사용하지 못 할 경우 결국 Data race를 발생시킬 수 있다.

이러한 문제점을 해결하기 위해 탄생한 것이 바로 Actor이다 !!

(와 여기까지가 5분 시청한거;; 말안돼 ;;)

Actor 란?

Actor는 공유 가능한 mutable state의 동기화를 위한 동기화 매커니즘(Synchronization mechanism)이다.

Actor는 자신만의 상태를 가지고있다. Actor의 상태는 나머지 프로그램으로부터 독집적인 상태로 그 상태는 Actor를 통해서만 접근할 수 있다.

Actor의 동기화 매커니즘은 해당 Actor의 상태에 서로 다른 두 개의 코드가 동시에 접근하지 않음을 보장한다. 이러한 특성은 Locks나 Serial dispatch queue를 사용해서 개발자가 직접 처리해주는 상호 배제 속성(mutual exclusion property)을 Swift에서 기본적으로 제공해주기 때문에 굉장히 편리하다 ! 추가로 동기화 코드를 작성하지 않으면 컴파일러가 컴파일 에러를 내뱉기 때문에 안전하게 동시성 프로그래밍을 할 수 있게 된다.

Actor는 Swift의 새로운 타입이다. Actor는 Swift에서 기존에 명명된 모든 타입들과 동일한 기능을 제공한다. Property, Method, Initializer, subscripts등 모두 사용할 수 있으며 protocol을 채택할 수도, extension을 활용하여 확장을 할 수도 있다.

Actor는 기본적으로 class와 같은 참조 타입이다. Actor의 탄생 배경을 생각해보면 당연한 이야기지만 Actor의 목적 자체가 공유 가능한 mutable state의 표현이기 때문이다. 그럼 class와 Actor의 차이점은 무엇일까?

Actor가 가지는 class와 구별되는 가장 주요한 특성은 바로 인스턴스 데이터를 나머지 프로그램으로부터 분리하고 데이터에 대한 동기화된 접근을 보장한다는 것이다.

위 예제 코드에서 Counter의 타입을 Actor로 바꾸면 value 프로퍼티에 동시 접근하지 않는 것을 보장하게 된다.

그리고 increment 메서드가 호출되었을 때, 해당 메서드가 완료 될 때까지 Counter의 다른 코드들은 실행되지 않는다. 이러한 특성은 Actor 타입의 객체 상태에 data race가 발생할 상황을 제거한다.

그럼 아까와 같이 서로 다른 2개의 Task 내부에서 increment 메서드를 호출하면 어떻게 될까?

위와 같이 작성하게 되면 Actor의 내부 동기화 매커니즘에 의해 하나의 Task 내부의 increment함수가 다른 Task 내부의 increment함수의 호출이 일어나기 이전에 완료되는 것을 보장해주게 된다. 이 덕분에 우리는 (위 Task부터 차례대로) 1, 2 혹은 2, 1의 결과를 얻을 수 있게 된다.

서로 다른 Task 내부에서 2번째 Task가 어떻게 자신의 차례까지 기다리는 것을 보장할 수 있을까?
Swift는 이러한 상황에 대한 매커니즘을 가지고 있다고 한다. (역시... Swift.. 그저 갓..)

+) 추가로 위 코드를 돌려보니 Actor 내부에 존재하는 메서드들은 기본적으로 async 키워드가 들어가있고 생략되어있는 형태인 것 같다. 외부에서 Actor 내부 메서드를 호출하기 위해선 await 키워드가 필수이다 !

Actor 외부에서 상호작용을 할 때, 우리는 비동기적으로 사용하게 될 것이다. 만약 Actor가 바쁘면 코드는 중단되고, 우리의 CPU는 다른 작업을 할 수 있게 된다. 이후 Actor가 한가해지면 코드가 다시 실행되면 Actor에서 호출된 함수가 실행되는 것이다.

await 키워드는 Actor에 대한 비동기 호출이 이러한 중단(suspension)을 포함할 수 있음을 나타낸다.

Counter 예시에 불필요한 작업을 집어넣어 반례를 확장시켜보자.

위 코드는 extension을 사용해 확장된 코드 블럭 내부에 메서드를 만들었기 때문에 resetSlowly라는 메서드를 Counter 내부에 존재하는 것이다. Actor 내부의 동기 코드는 항상 중단 없이 완료 될 때까지 실행되며 resetSlowly 메서드가 Actor 내부 메서드이기 때문에 increment메서드를 호출할 때 await 키워드를 붙이지 않고도 사용할 수 있다.

이는 Actor의 속성 중 하나이다. 이러한 속성 덕분에 동시성 프로그래밍의 side effect를 고려하지 않고도 동기 코드에 대한 순차적 추론이 가능해진다.

Actor 내부에서 동기 코드는 중단 없이 실행된다는 속성을 가지고 있다는 것을 강조하긴 했지만, 종종 Actor들은 서로 상호작용하거나 시스템 내의 다른 비동기 코드와 상호작용할 수 있다. 이런 경우엔 비동기 코드를 어떻게 처리하게 될까?

Actor Reentrancy

다른 비동기 코드와의 상호작용을 이해하기 위해서 아래와 같은 새로운 예시를 가져왔다.

이미지를 다운로드하고 캐싱하는 image라는 메서드를 가지고 있는 ImageDownloader라는 actor 타입의 객체가 있다고 가정하자. image라는 메서드의 logic flow는 간단하다. 같은 URL에서 여러번 다운로드 받는 것을 방지하기 위해 cache 내부에 저장된 이미지가 있는지 확인한다. URL에 해당하는 이미지가 없다면 image를 서버로부터 다운받고 그 이미지를 cache에 저장한 후 다운받은 이미지를 반환한다.

이 로직은 기본적으로 actor 내부에서 실행되기 때문에 low-level의 data race는 피할 수 있다. actor 내부의 동기 매커니즘 덕분에 cache 프로퍼티에는 한 번에 한 작업만 접근할 수 있고 동시성 프로그래밍을 하더라도 동시 접근이 일어날 수 없는 것이다.

하지만 중간에 await 키워드가 붙은 line을 유의해야한다. await이 붙었다는 것은 곧 해당 함수가 그 부분에서 중단될 수 있다는 것을 의미한다. 그리고 이는 CPU가 해당 함수를 중단시키고 프로그램 전반적인 상태에 영향을 줄 수 있는 다른 코드를 실행시킬 수 있다는 것을 의미한다. 즉, 중단되었던 함수가 재개될 때 프로그램의 전반적인 상태가 이미 변화된 후 일 수 있다. 우리는 이 점을 유의해야한다.

서로 다른 두 개의 Task에서 같은 시간에 동시에 같은 URL의 이미지를 불러오려고 하는 상황을 가정해보자.

Task 1에서 image 메서드를 실행시킬 때에는 캐싱된 이미지가 없을 것이고 "😸" 이미지 다운로드를 시작할 것이다. 서버로부터 이미지를 다운받는 작업은 시간이 걸리는 작업임으로 중단(suspend) 상태가 될 것이다.

Task 1에서 이미지를 다운받고 있는 동안, 서버에서 같은 URL에 존재하는 이미지가 바뀌었다고 해보자. 그리고 이 상태에서 이제 Task 2에서 동시 작업으로 같은 URL의 이미지를 불러고는 작업이 실행되는 것이다.

Task 1에서 이미지 다운로드가 아직 진행 중 임으로 같은 URL에 캐싱된 이미지를 아직 존재하지 않는다. 따라서 Task 2도 이미지 다운로드를 실행하게된다. 그리고 이 다운로드 또한 완료될 때까지 중단 상태가 된다.

이 때 중간에 서버에서 같은 URL에 존재하는 이미지가 바뀌었으므로 Task 2에서는 "😿" 이미지를 다운받게 된다.

이 상태에서 Task 1의 이미지 다운로드가 완료되면 해당 이미지를 캐시에 저장하고 "😸" 이미지를 반환한다.

이어서 Task 2의 이미지 다운로드가 끝남에 따라 같은 URL의 cache에 "😿" 이미지가 덮어씌워지고 Task 2에서는 "😿" 이미지를 반환하게된다.

이러한 이유로 우리는 캐싱을 했음에도 동일한 URL에 대해 다른 이미지를 받게 될 것이다. 이 뿐만 아니라 우리는 우리가 캐시를 clear하기 전까지는 항상 같은 URL에 따라 동일한 이미지를 항상 가져와야하는데 이러한 상황에서는 예상치 못하게 캐싱된 이미지가 바뀔 수 있는 것이다.

이 같은 상황은 위와같이 await이후의 상태를 확인함으로써 수정할 수 있다. 만약 우리가 image 함수를 재개했을 때, 캐싱된 이미지가 존재한다면 새로운 이미지는 버리고 오리지날 버전의 이미지를 사용하는 것이다. 더 나은 해결책은 중복되는 다운로드 자체를 아예 제거해버리는 것이다.

Actor의 재진입(reentrancy)는 교착상태(deadlocks)을 방지하고 순차적인 진행에 도움이 되지만, await을 기점으로 앞 뒤의 상태 변화를 체크해줘야한다.

재진입과 관련하여 설계를 잘 하기 위해서는 Actor의 상태 변화를 동기 코드 내부에서 수행해야한다. 여기서 상태의 변화는 Actor를 일관되지 않은 상태가 되는 것이 포함된다.

그리고 await은 잠재적 중단 상태 지점이라는 것을 명심해야한다 ! 코드가 중단 상태가 되면, 우리의 코드가 재개되기 전에 프로그램 내부에서 어떤 알지 못하는 변화가 일어날지 우리는 알 수 없다.

 

아놔... 영상 20분따리인데 이제 12분 봤다.. 이게 말이되나.... WWDC21은 왜 한국어 자막 제공 안해주는데요 ㅠㅜㅠㅜㅠㅜ
여기까지도 내용이 너무 많기 때문에(내 기준).. 일단 여기까지 정리하고 다음 글에서 이어서 정리하도록.. 하겠다.. ^_^

살짝의 스포(?)를 해보자면 다음 주제는 이제 Actor의 isolation 관련한 주제이다.. 🤯 🤯 🤯

저작자표시 (새창열림)

'Swift' 카테고리의 다른 글

[Swift] Task  (8) 2024.10.25
[Swift] Property wrapper  (0) 2024.10.06
[Swift] Swift 빌드 과정  (2) 2024.09.08
[Swift] Value Type과 Reference Type  (4) 2024.09.01
'Swift' 카테고리의 다른 글
  • [Swift] Task
  • [Swift] Property wrapper
  • [Swift] Swift 빌드 과정
  • [Swift] Value Type과 Reference Type
00me
00me
얼렁뚱땅 방장이 운영하는 기술 블로그
  • 00me
    영미의 iOS 다이어리
    00me
  • 전체
    오늘
    어제
    • 프로그래밍 (29)
      • 📖 (0)
      • CS (4)
      • Python (5)
      • Swift (5)
      • iOS (3)
      • 코테 (3)
        • 자료구조 (0)
        • 알고리즘 (3)
      • 회고 (9)
  • 링크

    • 🍧 GitHub
  • 인기 글

  • hELLO· Designed By정상우.v4.10.3
00me
[Swift] Actor(1)
상단으로

티스토리툴바