SwiftConcurrency 1편 부터 이어지는 내용입니다. 꼭 1편부터 보고 와주시길 바랍니다.
Reentrancy
란 동일한 함수가 이미 호출중 일경우 다시 호출될수 있는 것을 말합니다.
즉 다시말해
Actor reentrancy
는actor
의 메서드가 다른 작업을 수행할때 (중일때)
다시 호출될수 있음을 말하죠.
import Foundation
actor MyActor {
private var value = 0
func increment() {
value += 1
print("Incremented value to \(value)")
}
func performAsyncWork() async {
print("엑터 시작")
await Task.sleep(for: .seconds(1)) // 1초 대기
print("엑터 끝")
}
func performWork() async {
print("performWork 시작")
await performAsyncWork()
print("performWork 끝")
}
}
let myActor = MyActor()
Task {
await myActor.performWork()
}
Task {
await myActor.increment()
}
하지만 만약 저장소와 원격 저장소를 동기화 하는 경우일 떄는 어떨까요?
import Foundation
actor UserStore {
private var store: [String: String] = [:] // 내부 저장소: 아이디 -> 이름
// 원격 저장소에서 데이터를 가져오는 메서드 (모의 구현)
private func fetchFromRemoteStore(id: String) async -> String {
// 원격 서버와 통신하는 시간이 걸림을 모의
await Task.sleep(for: .seconds(1)) // 1초 대기
return "RemoteUserName" //
}
// 동기화 메서드
func remoteSynchronize(id: String) async -> String {
if let name = store[id] {
return name
}
// 원격 저장소에서 사용자 정보를 가져와 동기화
let remoteName = await fetchFromRemoteStore(id: id)
store[id] = remoteName
return remoteName
}
}
A 쓰레드가 B 쓰레드와 동일한 아이디로 remoteSynchronize
를 호출하게 됩니다.
A가 내부 저장소를 확인후 -> 해당 아이디의 정보가 없음을 확인하겠지요
A가 원격 저장소에서 데이터를 가져오는 동안에 await
키워드를 만나 값이 오기까지 대기합니다.
B가 실행되어 동일한 아이디로 내부 저장소를 확인하고 -> 데이터가 없음을 확인합니다.
B도 원격 저장소에서 데이터를 가져오려고 시도합니다 즉 await
키워드를 만나 값이 오기까지 대기합니다.
A가 원격 저장소에서 데이터를 받아와 내부 저장소에 저장합니다.
B도 마찬가지로 내부 저장소에 저장하겠죠.
동일한 요청이 발생하는구나!
이는 즉 중간에 바뀐 정보가 있다고 하면 다른 결과값을 받아오게 되겠어요!
이 상황을Actor reentrancy
라고 합니다.
actor
가 멀티스레드 환경에서 데이터 경쟁을 방지하기 위한 개념으로,
actor
내의 가변 상태는 외부에서 직접 접근할 수 없으며,
모든 접근은actor
인스턴스 메서드를 통해 이루어지게 해요!
이는actor
가 내부적으로 serial queue(직렬화 큐)를 사용하여 동작하기 때문에,
동시성 문제를 해결할 수 있어요!
actor
상태에 접근 하려면self
키워드를 통해 접근이 가능해요!
클래스 혹은 구조체self
와 매우 유사하죠.
actor MyActor {
private var count: Int = 0
func increment() {
self.count += 1
}
func getCount() -> Int {
return self.count
}
}
만약
Actor
외부에서actor
의 상태나 메서드에 접근 해야 한다면,
비동기 메서드를 호출하여 접근해야 합니다.await
키워드 함께요!
/*
func increment() async {
self.count += 1
}
*/
let myActor = MyActor()
Task {
await myActor.increment()
let currentCount = await myActor.getCount()
print("Current count: \(currentCount)")
}
isolated
키워드는 특정 메서드가actor
의 격리된 상태 에서 호출됨을 명시하는 키워드 에요!
즉 안전하게actor
의 상태에 접근할 수 있게 됩니다.
주로actor
의 메서드 매개변수로 사용되어, 해당 메서드가 특정
actor
인스턴스의 격리된 상태에서 호출된다는 것을 명시합니다
즉, 안전성을 높이고, 동시성 문제를 관리할 수 있게 되죠!
actor MyActor {
private var count: Int = 0
func increment() {
self.count += 1
}
func getCount() -> Int {
return self.count
}
// `isolated` 키워드를 사용한 메서드
func resetCount(isolatedTo actor: isolated MyActor) {
actor.count = 0
}
// `isolated` 키워드를 사용하지 않은 메서드
func resetCount(actor: MyActor) {
actor.count = 0 // 오류: 'count'에 접근할 수 없습니다.
}
}
let myActor = MyActor()
Task {
await myActor.increment()
let currentCount = await myActor.getCount()
print("Current count: \(currentCount)")
// `isolated` 키워드를 사용한 메서드 호출
await myActor.resetCount(isolatedTo: myActor)
let resetCount = await myActor.getCount()
print("Count after reset: \(resetCount)")
}
isolated
키워드를 사용하지 않는 경우도 작성해 보았습니다.
하지만 위와 같은 에러가 발생합니다. 이유가 무엇일까요?
actor
내부의 상태는 외부에서 직접 접근하거나 수정하는 것을 허용하지 않아요...
isolated
키워드를 명시하여.actor
의 격리된 상태에서 안전하게 상태를 수정할 수 있게 되죠.
자 이번에도 거침없이 무지막지한 내용을 가지고 와봤습니다.
... 하지만 아직도 끝이 아니죠 ㅎㅎ 다음 시간에는nonisolated
와 드디어 저번에 예고했던
Sendable
편으로 돌아와 보겠습니다. 고생하셨습니다~