동시성 프로그래밍에서 data race를 방지하고 안전하게 상태를 관리하기 위한 새로운 참조 타입
여러 스레드에서 동일한 메모리 위치를 읽거나 쓰려고 할 때 발생하는 문제
ex) 동일한 하나의 객체의 프로퍼티에 2개의 스레드가 추가, 삭제 작업을 동시에 진행함으로써 잘못된 데이터 발생
class Counter {
private var count = 0
func increase() {
count += 1
}
}
increase 메서드가 여러 곳에서 비동기적으로 동작data race 발생var counter = Counter()
DispatchQueue.global().async {
counter.increase() // mutating - 공유된 인스턴스 변경 → 위험!
}
DispatchQueue.global().async {
counter.increase() // 동시에 접근하면 data race 발생 가능!
}
구조체도 피할 수 없다.
- class와 유사한 새로운 타입
- 프로퍼티, 메서드, 이니셜라이저 등등을 모두 가질 수 있음.
- 프로토콜, extension도 가능
- 참조타입
- 상속 지원 x
actor Counter {
var count: Int = 0
func increase() {
count += 1
}
}
//외부
let counter = Counter()
await counter.increase()
await으로 비동기적으로 호출되게 함.actor Counter {
var count: Int = 0
func increase(diffCounter: Counter) {
self.count += 1
diffCounter.count += 1 🚨 에러 발생!
actor 내부 프로퍼티는 무조건 self로 접근되어야 함.
}
}
instance 메서드는 기본적으로, actor-isolated 상태.self로 접근되어야 함.actor Counter {
var count: Int = 0
let name: String = "hello"
func increase(diffCounter: Counter) {
self.count += 1
print(diffCounter.name)
}
}
await을 동한 비동기 접근만 가능name이 var로 변경될 경우를 대비하기 위함.actor Counter {
var count: Int = 0
let name: String = "hello"
func increase(diffCounter: Counter) {
self.count += 1
power() // 동기 호출 가능
await diffCounter.decrease() 비동기로 호출
}
}
extension Counter {
func decrease() {
self.count -= 1
}
func power() {
self.count *= selfcount
}
}
해당 메서드, 프로퍼티가 actor의 격리된 상태에 안전하게 접근할 수 있음을 명시
이 코드는 반드시 특정 Actor의 보호 영역(실행 컨텍스트) 안에서 실행되어야 한다"고 명시
기본적으로 actor 내부의 메서드, 프로퍼티는 isolated로 간주
actor BankAccount {
let accountNumber: Int
var balance: Double
init(accountNumber: Int, initialDeposit: Double) {
self.accountNumber = accountNumber
self.balance = initialDeposit
}
func deposit(amount: Double, to account: BankAccount) {
assert(amount >= 0)
account.balance = account.balance + amount // Actor-isolated property 'balance' can not be referenced from a non-isolated context
}
}
위의 deposit 메서드를 보면
매개변수로 들어온 account 계좌에 amount 만큼 입금하려는 동작을 하려한다.
하지만 들어온 매개변수인 account 객체 입장에서 deposit 메서드는 non-isolated context이기 때문에 참조할 수 없다.
이걸 해결하기 위해 isolated 키워드가 사용된다.
actor BankAccount {
let accountNumber: Int
var balance: Double
init(accountNumber: Int, initialDeposit: Double) {
self.accountNumber = accountNumber
self.balance = initialDeposit
}
func deposit(amount: Double, to account: isolated BankAccount) {
assert(amount >= 0)
account.balance = account.balance + amount
}
}
매개변수에 isolated를 키워드를 추가해주어 접근이 가능하게 된다.
이 키워드를 사용하면 선언됨 함수가 해당 actor의 실행 컨텍스트 내에서 실행됨을 보장.
Actor의 멤버(메서드, 프로퍼티)가Actor의 격리된 상태에 포함되지 않음을 명시
기존 actor 내부의 값에 접근하려면 await 키워드를 이용해 데이터 레이스가 발생하지 않을 시점에 비동기적으로 접근하였으나, 동기적으로 접근 할 수 있게 해줌.
Actor의 보호를 받지 않음을 명시했기 때문
actor Counter {
var count: Int = 0
nonisolated let name: String = "hello"
func increase(diffCounter: Counter) {
self.count += 1
await diffCounter.decrease()
}
}
extension Counter {
func decrease() {
self.count -= 1
}
}
//기존
let name = await Counter.name
//nonisolated 적용 시
let name = Counter.name
print(name)
var에 적용 시에는 불가. data-race 문제가 발생할 수 있기 때문에 컴파일 에러 발생actor는 내부 메서드가
await과 같은 키워드로 일시 중단될 경우, 다른 작업 처리 가능
actor Counter {
private var value = 0
func increment() async {
value += 1
await Task.sleep(1_000_000_000) // 1초 대기
value += 1
}
func reset() {
value = 0
}
}
Counter 객체가 increment 메서드를 호출하고 중단된 시점동안 reset 메서드 동작 가능
struct Main {
static func main() async {
let counter = Counter()
// increment 실행 중 일시 중단 지점이 있으므로
// 그 사이에 reset이 호출되면 value 상태가 예상과 달라질 수 있음
Task {
await counter.increment()
}
// 0.3초 뒤에 reset 호출
Task {
try? await Task.sleep(nanoseconds: 300_000_000)
await counter.reset()
}
// 2초 뒤 최종 결과 출력
try? await Task.sleep(nanoseconds: 2_000_000_000)
let final = await counter.getValue()
print("Final value: \(final)")
1? 0?
}
}
Counter객체 내부의 프로퍼티를 접근하는 여러 메서드를 호출할 경우, data-race 문제가 있음.
DispatchQueue.main에서 작업을 실행하는 excutor를 제공.
메인 Thread에서 실행되어야 하는 작업을 안전하게 처리 가능
클래스 및 구조체 내부의 모든 메서드 및 프로퍼티가 메인 Thread에 동작하게 적용하는 방법
@MainActor
class ViewModel {
var title: String = "Hello, Swift!"
func updateData() {
// UI 업데이트 코드
}
}
특정 메서드, 프로퍼티가 메인 Thread에 동작하게 적용하는 방법
class ViewModel {
@MainActor var title: String = "Hello, Swift!"
@MainActor
func updateUI() {
// UI 업데이트 코드
}
}
특정 클로저가 메인 Thread에 동작하게 적용하는 방법
func fetchData(completion: @MainActor @escaping () -> Void) {
Task {
await someBackgroundOperation()
await completion()
}
}
completion이 메인 Thread에서 동작하도록 적용
Task {
await MainActor.run {
// 메인 스레드에서 실행할 코드
}
}
GCD, Combine 방식에 적용하더라도 동작 X, 수동으로 메인 스레드로 전환 해주어야 함.(DispatchQueue.main)context에서 호출 시 메인 Thread가 아닌 곳에서 호출될 수 있음.@MainActor
class Test {
var state: Bool = false {
didSet {
print(Thread.isMainThread) // false
}
}
var subscriptions: Set<AnyCancellable> = []
init() {
Just(true)
.receive(on: DispatchQueue.global(qos: .background))
.sink {[weak self] _ in
self?.updateState()
}.store(in: &subscriptions)
}
func updateState() {
print(Thread.isMainThread) // false
state.toggle()
}
}
MainThread에 돌아간다는 것을 적용했어도 Combine코드에서 Thread를 따로 지정할 경우, MainThread에서 돌아감을 보장X
@MainActor
func updateUI() {
print("UI updated on thread: \(Thread.isMainThread ? "Main Thread" : "Background Thread")")
}
// 동기 context에서 호출
func someFunction() {
updateUI() // ⚠️ 이 코드는 메인 스레드에서 실행된다는 보장이 없음
}
동기 함수는 호출된 스레드에서 즉시 실행되기 때문에 Thread를 지정해주어도 호출된 Thread에서 동작
Swift 6에서 컴파일 할 경우, 해당 부분들은 모두 경고를 통해 확인할 수 있음 (엄격한 동시성 검사 활성화 시)
출처:
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0306-actors.md
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0313-actor-isolation-control.md
https://www.swiftwithvincent.com/blog/discover-how-main-actor-works-in-swift?utm_source=chatgpt.com
https://forums.swift.org/t/using-mainactor-to-ensure-execution-on-the-main-thread/60764