티스토리 뷰

Swift/Concurrency

Swift ) Actor (1)

Zedd0202 2021. 8. 22. 22:10
반응형

 

안녕하세요 :) Zedd입니다.

WWDC 21 ) What‘s new in Swift 에서도 잠깐 본 내용인데, Actor에 대해서 공부. 

 

# 다중 쓰레드 시스템에서 제대로 작동하지 않는 코드

WWDC 21 ) What‘s new in Swift  에서 본 예제. 

class Counter {
    var count: Int = 0
    
    func increment() {
        self.count += 1
    }
}

이런 Counter가 있고,

let counter = Counter()
// global
DispatchQueue.global().async {
     counter.increment()
}
// main
counter.increment()

이렇게 각기 다른 스레드에서 increment를 호출하면,

대충 이런 경고를 받을 수 있다. (crash를 기대했는데...crash는 안남)

 

# concurrent programs

이렇게 concurrent programs(동시 프로그램?) 작성에 있어 근본적으로 어려운 문제 중 하나는 data race를 피하는 것

data race : 2개 이상의 개별 쓰레드가 동시에 동일한 데이터에 접근하고, 이러한 접근 중 하나 이상이 write일 때 발생. 

data race는 만들기는 쉽지만

1. 디버깅하기는 굉장히 어려움. 

2. OS의 스케쥴러가 프로그램을 실행할 때 마다 다른 방식으로 concurrent tasks를 인터리빙할 수 있기 때문에 비결정적.

인터리빙 : 컴퓨터 하드디스크의 성능을 높이기 위해 데이터를 서로 인접하지 않게 배열하는 방식.

 

# data race가 발생하지 않으려면

결국 data race가 발생하는 원인은 데이터가 shared mutable state이기 때문

데이터가 변경되지 않거나, 공유되지 않는 경우 data race는 발생할 수 없음

 

즉! data race를 피하려면

value semantic을 사용하여 shared mutable state를 제거하면 된다. 

- value-semantic type인 "let"은 immutable하므로 동시에 발생하는 서로 다른 작업에 대해 안전.

- Swift Standard Library에 있는 대부분의 타입은 value-semantic을 가짐(array, dictionary..)

이러한 value semantic이 data race를 해결할 수 있도록 설계되었으니..class였던 Counter를 struct로 변경시켜보자. 

struct Counter {
    var count: Int = 0
    
    mutating func increment() {
        self.count += 1
    }
}

(struct로 바꾸고 count를 수정하려면 increment 메소드 앞에 mutating 키워드를 붙혀야함)

counter가 상수인 let으로 선언되었으므로 mutating 메소드를 호출 할 수 없음.

🙋 :  아하 struct로 선언했으니까 된거지? 그럼 var counter = Counter()로 바꾸자~!!

🧑‍💻 :  위에서 뭔가 Value Semantic이면 race condition이 안날 것 같이 설명했는데..

그건 아님. var로 선언하면 data race 발생한다. 

🙋 :  아니 그럼 어쩌라고...나는 concurrent program을 짜야하는데..**

야 내가 lock / serial dispatch queues같은거 써야하냐고 야 내가 써야겠냐고 그거를 

(위 lock이나 serial dispatch queues는 data race를 막는 방법들임. 데이터 접근을 동시에만 안하면 되니깐)

내가 얘네 쓰려면 매번 반드시 정확하게 신중히 써야한단 말이야 ㅡㅡ 안그러면 무조건 data race 난단 말이여

🧑‍💻 : Actor 어서오고

🙋 : ⁉️ 

 

# Actor

Actor는 shared mutable state에 대한 동기화 메커니즘임

Actor는 자신만의 상태(own state)를 가지며, 해당 상태는 프로그램의 나머지 부분과 분리되어있음.

해당 상태에 접근하는 유일한 방법 → Actor를 거치는 것.

 

Actor를 거칠 때 마다 Actor의 동기화 메커니즘이 Actor의 상태에 동시에 접근하지 않도록 함. 

→  lock이나 serial dispatch queues를 사용하는 것과 동일하게

상호배제(mutual exclusion)를 제공하지만, Actor를 사용하면 Swift가 근본적으로 보증한다.

(it is a fundamental guarantee provided by Swift)

 

# Actor의 사용

목적 : shared mutable state를 표현하는 것. 

- Actor는 그냥 Swift의 새로운 타입임. 클래스와 가장 유사. 

- Swift의 다른 모든 타입들과 똑같이 프로퍼티, 메소드, 이니셜라이저, subscripts 등을 가질 수 있음.

- 프로토콜 준수, Extension 역시 쌉가능

- 참조타입 like class

- 클래스와 달리 Actor는 한번에 하나의 작업만 변경 가능한 상태(mutable state)에 접근할 수 있도록 허용.

- 클래스와 달리 상속을 지원하지 않음.

나는 왜 이렇게 어색한지 모르겠는데;;; 그냥 

class Counter {} 

이렇게 쓰던걸

actor Counter {}

이런식으로 쓰면 된다.

[특징] 

- 프로그램의 나머지 부분에서 인스턴스 데이터를 분리

- 해당 데이터에 대한 동기화 된 접근을 보장.

 

Counter를 Actor를 사용하게 바꿔보자. 

actor Counter {
    var count: Int = 0
    
    func increment() {
        self.count += 1
    }
}

class 때랑 개똑같음. 

차이점은 Actor가 값이 동시에 접근되지 않도록 보장한다는 것 뿐.

즉, 위 Counter로 예를 들면,

increment 메소드가 호출 될 때 actor에서 다른 코드가 실행되지 않고 완료가 된다는 뜻 

(대충 한번에 하나의 접근만 실행한다는 뜻)

이렇게 되면 data race가 일어날 일이 없음.

 

계속 말하지만 하나의 작업이 actor에 들어오면, 나머지 하나(두번째 작업)는 "기다려야한다"

🙋 : 그럼 두번째 작업이 자기 차례를 잘 기다릴 수 있도록 어떻게 보장해?

🧑‍💻 : Swift에는 이를 위한 메커니즘이 있음. 외부에서 Actor와 상호작용할 때 마다 비동기식으로 수행함.

특정 변경을 수행하는 것이 안전할 때 까지 (데이터 손상(corruption)을 일으킬 수 있는) 작업을 일시 중단하여 작동함.

이 부분 굉장히 그냥 술술 읽고 넘어갈 수 있는데, 굉장히 중요한 부분이다.

Actor는 data race를 피하기 위해서 잠시동안 호출코드를 "기다리게" 할 수 있음

그래서 Actor는

func someMethode() async {
  let counter = Counter()
  await counter.increment()
}

보통 외부에서 호출할 때는 async / await과 함께 쓰게 될 것. 

이 부분은 Actor isolation과 관련이 있는데..요건 다음글에서 자세히 다루도록 한다. 

 

📝 Actor (2) 읽으러가기

 

 

반응형