Search

[정리] 배우신분 Actor

Actor를 알아보겠습니다
contents

Actor

Actor도 저번주에 발표했던 async / await 와 마찬가지로 Swift 5.5부터 추가된 기능입니다. Concurrency와 관련된 문제를 해결하기 위해서 도입됐습니다.
async / await를 사용하면 여러 쓰레드에서 쉽게 작업할 수 있지만 문제가 한가지 있습니다.
다른 쓰레드에서 실행한다고 하더라도 개별 쓰레드가 데이터를 공유할 때마다 데이터가 일치하지 않거나 손상될 위험이 있습니다.
이러한 위험으로부터 데이터를 보호하는데 Actor가 도움이 됩니다.
Actor는 한번에 하나의 일만 수행하도록 만들 수 있습니다. +++ 기존에도 동시성 문제를 해결하는 방법이 존재했습니다. 각종 Lock, DispatchQueue 등을 활용하는 방식 ==> Actor를 사용하면 컴파일러가 근본적으로 동시성 문제를 확인하고 오류를 일으킬 수 있습니다.
Actor : Swift의 동시성 모델 - 다양한 동시성 버그들로부터 안전한 프로그래밍 환경을 제공하기 위해 만듦 - 각자 다른 스레드에서 같은 데이터에 접근할 때 생기는 문제 해결(data race condition) - 하나의 동시성 영역안에서 유지되는 상태의 모임 생성 - 각자의 데이터를 'Actor Isolation'이라는 개념을 이용해 보호 - 한 데이터에 동시에 단 하나의 스레드만 접근할 수 있도록 함

actor type

actor는 새로운 타입으로 개발
actor BankAccount { let accountNumber: Int var balance: Double init(accountNumber: Int, initialDeposit: Double) { self.accountNumber = accountNumber self.balance = initialDeposit } }
Swift
복사
1.
reference type(class와 가장 유사)
2.
initializer, 메서드, 프로퍼티, subscript를 가질 수 있습니다.
3.
프로토콜 준수, Extension 가능
4.
한번에 하나의 작업만 변경 가능한 상태(mutable state)에 접근할 수 있도록 허용
5.
상속을 지원하지 않음
6.
actor로 정의된 경우엔 data race를 방지할 수 있도록 Actor Isolation를 지키며 작성되어야 합니다.
→ 컴파일러단에서 검사를 해주기 때문에 Safe한 프로그램 가능
actor에 하나의 작업이 들어오면 나머지 하나는 "기다려야 한다" 어떻게 보장할 수 있는가? - Swift에는 이를 위한 메커니즘이 존재. - 외부에서 Actor와 상호작용할 때마다 비동기식으로 수행 - 특정 변경을 수행하는 것이 안전할 때까지 작업을 일시 중단하여 작동 !!!!!Actor는 data race를 피하기 위해서 잠시동안 호출코드를 기다리게 할 수 있다!!!!!!!

Actor Isolation

Actor의 mutable(가변)한 상태를, Actor를 고립시킴으로써 보호
actor내에 정의된
1.
stored, computed instance properties
2.
instance methods
3.
instance subscripts
는 모두 actor-isolated한 상태
==> actor-isolated는 기본적으로 self를 통해서만 접근이 가능하다
가장 기본적인 원칙
Actor의 프로퍼티에는 오직 self를 통해서만 접근할 수 있도록 한다.
actor-isolated 정의들은 다른 actor-isolated 정의들을 자유롭게 참조 할 수 있다.
actor-isolated가 아닌 정의들은 actor-isolated 정의들에 동기적으로 접근할 수 없다.
extension BankAccount { enum BankError: Error { case insufficientFunds } func transfer(amount: Double, to other: BankAccount) throws { if amount > balance { throw BankError.insufficientFunds } print("Transferring \(amount) from \(accountNumber) to \(other.accountNumber)") balance = balance - amount other.balance = other.balance + amount // error } }
Swift
복사
마지막 줄에서 에러가 발생

Cross-Actor Reference

actor로 정의한 객체를 외부에서 접근하기
Cross-Actor Reference가 제대로 작동하는 경우는 두 가지이다.
1.
접근하는 프로퍼티가 immutable할 경우
위의 예제에서 accountNumber은 상수로 정의되어 있기 때문에 값이 변할 일이 없어서 data race 문제가 발생하지 않는다. 따라서 상수에 접근하는 것을 self로 막지 않고 누구나 접근 가능하도록 했다.
같은 모듈안에 정의되어 있으면 let이기 때문에 접근이 가능하지만, 외부 모듈에서는 반드시 비동기 호출로 참조해야한다.(await)
2.
Cross-Actor Reference가 비동기 함수 안에서 등장하는 경우
await가 붙어있는 명령은 당장 그걸 실행하는 것이 아니라, 명령을 차후에 실행시켜달라고 해당 Actor에 메세지를 보내는 것과 같다. Actor는 다른 작업을 끝내고 나서 메세지를 하나씩 꺼내서 처리하도록 할 수 있다.
== 동시적으로 객체에 접근하게 될 가능성이 없어진다.
== data race 걱정을 하지 않아도 된다.
extension BankAccount { func transfer(amount: Double, to other: BankAccount) async throws { assert(amount > 0) if amount > balance { throw BankError.insufficientFunds } print("Transferring \(amount) from \(accountNumber) to \(other.accountNumber)") balance = balance - amount await other.deposit(amount: amount) } func deposit(amount: Double) { assert(amount >= 0) balance = balance + amount } }
Swift
복사
transfer에는 await가 달려있기 때문에 transfer를 호출한 BankAccount는 other에서 작업이 완료될 때까지 Suspend된다.
소연이가 인애에게 5000원을 보내고 싶어서 소연.transfer(amount: 5000.0, to: 인애) 내부 await other.deposit(amount: amount)에서 인애에게 너의 deposit를 실행하라는 메세지를 보내준다. → 소연이의 작업흐름은 suspend 인애는 할 일을 하고 있다가 소연이의 deposit 실행 요청을 보고 하던 일을 마친 후에 요청을 꺼내서 실행한다. 인애의 실행이 완료되면 인애는 다른 일을 실행하고 소연이의 작업흐름은 재실행한다. - self로만 접근한다는 원칙 깨지지 않음 - 한번에 하나의 작업만 할 수 있기 때문에 프로퍼티에 여기저기서 한번에 접근해 data race 발생할 가능성 없음
︎ 하지만 프로퍼티 set 접근을 해서는 안된다. 모두 read-only인 경우에만 가능하며, set으로 직접 프로퍼티 변경은 불가능하다.
func checkBalance(account: BankAccount) async { print(await account.balance) // ok await account.balance = 2000.0 // error }
Swift
복사

Compile-time actor-isolation checking

컴파일 시간에는 actor-isolation checking이 이루어진다.
1.
cross-actor reference인지 확인
2.
이러한 참조가 2가지 메커니즘 중 하나를 사용하도록 한다.(cross-actor reference)
이렇게 하면 actor 외부 코드가 actor의 mutable state를 방해하지 않는다. == 보호 방법
++ 원래 Actor 내부에서 동기적으로 호출이 가능하다(await를 안써줘도 된다)
하지만 cross-actor reference일 때는 비동기 호출이 필요하기 때문에 await를 써줘야 한다.

Closure

Closure에서도 data race가 발생할 가능성이 없도록 안전하게 설계해야 함
여러 클로저가 동시에 실행되는 상황이 된다면 문제가 생길 수 있다.
accounts.parallelForEach { account in // 각 Closure가 동시에 돌아가는 parallelForEach self.balance = self.balance - transferAmount // Data race await account.deposit(amount: transferAmount) }
Swift
복사
DATA RACE 발생할 가능성 높다
extension BankAccount { func close(distributingTo accounts: [BankAccount]) async { let transferAmount = balance / accounts.count accounts.forEach { account in // actor-isolated balance = balance - transferAmount await account.deposit(amount: transferAmount) } await thief.deposit(amount: balance) } }
Swift
복사
DATA RACE 발생할 가능성 없어짐

한계 해결하기

actor-isolated 상태때문에 actor는 한계가 존재합니다.
1.
deposit 메소드는 반드시 actor의 인스턴스로만 접근 가능
2.
actor 외부에서 synchronously하게 사용할 수 있는 computed property를 만들 수 없다.
actor BankAccount { let accountNumber: Int var balance: Double let accountName: String var displayName: String { return self.accountName + "안녕하세요" } init(accountNumber: Int, initialDeposit: Double, accountName: String) { self.accountNumber = accountNumber self.balance = initialDeposit self.accountName = accountName } }
Swift
복사
외부에서 BankAccount 인스턴스를 만들고 displayName를 호출하려고 하면 컴파일 에러가 납니다.
await로만 호출이 가능합니다.
3.
Hashable comform 불가
hash(into:)가 actor외부에서 호출될 수 있기 때문에
개선방법
함수의 파라미터 중 어떤 것이 isolated인지 표시
선언 자체를 actor로부터 isolated하지 않음으로써 actor-isolated를 선택할 수 있도록 함
함수의 파라미터 중 어떤 것이 isolated인지 표시
func deposit(amount: Double, to account: isolated BankAccount) { assert(amount >= 0) account.balance = account.balance + amount }
Swift
복사
account 파라미터에 isolated로 표시하게 되면, deposit은 actor-isolated함수가 된다.
actor-isolated 상태에 직접 접근 가능
nonisolated
actor BankAccount { let accountNumber: Int var balance: Double let accountName: String nonisolated var displayName: String { return self.accountName + "안녕하세요" } init(accountNumber: Int, initialDeposit: Double, accountName: String) { self.accountNumber = accountNumber self.balance = initialDeposit self.accountName = accountName } }
Swift
복사
nonisolated를 붙히게 되면 actor-isolated 상태인데, 이 동작을 비활성화 할 수 있게 됩니다.
let account = BankAccount(accountNumber: 1, initalDeposit: 100, accountName: "Duna") print(account.displayName) // Duna 안녕하세요
Swift
복사
하지만,
actor BankAccount { nonisolated let accountNumber: Int nonisolated var balance: Double // error nonisolated let accountName: String nonisolated var displayName: String { return self.accountName + "안녕하세요" } init(accountNumber: Int, initialDeposit: Double, accountName: String) { self.accountNumber = accountNumber self.balance = initialDeposit self.accountName = accountName } }
Swift
복사
let이 아닌 var는 concurrent code에서 data race가 발생할 수 있기 때문에 막아뒀습니다.
++ 불변 상태에 대한 cross-actor reference는 같은 모듈 내에서만 적용이 되었는데, nonisolated면 다른 모듈에서도 동기적으로 참조가 가능합니다.
actor BankAccount { public nonisolated let accountNumber: Int ... } // 다른 모듈 func outside_moudle_method() { let account = BankAccount(accountNumber: 1, initialDeposit: 10) print(account.accountNumber) }
Swift
복사

Actor의 프로퍼티를 직접 변경하지 않도록 코드를 작성하기
Actor의 내부는 동기적이게 유지하고 외부에서 Actor를 참조할 때는 비동기적으로 접근할 수 있도록 해야 함

참고자료