[iOS] Actor 알아보기

z-wook·2024년 1월 14일
post-thumbnail

Actor는 Swift의 동시성 모델에서 데이터 레이스를 방지하도록 설계되었습니다. Actor를 사용하면 여러 스레드에서 안전하게 데이터를 공유하고, 동시에 실행되는 코드 블록에서 Data race가 발생하지 않도록 보장할 수 있습니다. 그렇다면 Data race가 무엇인지부터 알아보겠습니다.

Data race란?

데이터 레이스(Data race)란 멀티 스레드/프로세스 환경에서 일어나는 오류이며, 여러 스레드/프로세스가 공유자원에 동시에 접근하려 할 때 일어나는 경쟁 상황을 뜻합니다.

즉, 한 스레드와 또 다른 스레드가 동시에 한 변수를 쓰려고 할 때, 한 스레드는 데이터를 쓰고 있는데 다른 스레드는 그 데이터를 읽으려고 할 때 등의 상황에서 데이터 레이스가 발생합니다.

데이터 레이스는 일반적으로 병렬 처리 환경에서 발생하며, 여러 스레드가 동일한 데이터를 변경하거나 읽을 때 발생할 수 있습니다.

Data race가 발생하는 상황을 직관적으로 보기 위해 아래의 코드를 실행해 보면

Data race가 발생하는 코드

class Counter {
    private var count: Int = 0
    
    func increase() -> Int {
        self.count += 1
        return self.count
    }
}

@IBAction func actorDidTap(_ sender: Any) {
	let counter: Counter = .init()

	// Task.detached로 생성된 작업은 백그라운드에서 실행되며, 
	// 작업이 완료될 때까지 대기하지 않고, 작업을 생성하고 그 즉시 다음 코드로 진행합니다.
	Task.detached {
		for _ in 0..<25 {
			try? await Task.sleep(nanoseconds: 1_000_000_000 / 100)
			print("count1 : \(counter.increase())")
		}
	}
	Task.detached {
		for _ in 0..<25 {
			try? await Task.sleep(nanoseconds: 1_000_000_000 / 100)
			print("count2 : \(counter.increase())")
		}
	}
}

Data race 결과

두 개의 Task에서 하나의 Counter 클래스의 동일한 count 변수에 쓰는 작업을 하면서 순서도 보장되지 않고, 그에 따라 예측할 수 없는 결과를 초래하고 있습니다.

따라서 Data race를 피하면서, Task 간에 안전하게 가변 데이터를 공유하기 위해 Actor가 등장했습니다.


Actor란?

Actor는 외부로부터 데이터를 격리하고, 한 번에 하나의 Task만 내부 상태를 조작하도록 허용함으로써 동시 변경으로 인한 Data race를 피합니다.

Actor에는 동시에 하나의 Task만 접근할 수 있습니다. 따라서 Actor의 변경 가능한 프로퍼티도 여러 스레드에서 동시에 접근되지 않습니다.

Actor의 주된 특징은 인스턴스 데이터를 프로그램의 나머지 부분에서 격리(isolate) 하고, 해당 데이터에 대한 동기화된 액세스를 보장한다는 것입니다.

Actor는 여러 스레드에서 동시에 실행되지 않습니다. 즉, Actor에 대한 접근은 직렬화(serialized) 됩니다.

Actor 요약

  • 여러 Task로부터 공유자원에 대한 접근 관리
  • 공유자원을 격리(isolate) 시키고, 공유자원에 대해서 Serial 하게 접근하도록 처리. 이를 통해 Data race 해결
  • Actor 내부의 property는 외부에서 mutate 할 수 없음. Actor 내에서만 mutate 가능
  • Actor는 기본적으로 Reference Type
  • 격리 시키지 않을 메서드는 nonisolated 키워드를 붙여서 제외 가능

Actor 사용 코드

actor Counter {
    private var count: Int = 0
    
    func increase() -> Int {
        self.count += 1
        return self.count
    }
    
    // 격리 시키지 않을 메서드는 nonisolated 키워드를 사용하여 제외할 수 있습니다.
    nonisolated func printText() {
        print("non isolated method")
    }
}

@IBAction func actorDidTap(_ sender: Any) {
	let counter: Counter = .init()
	counter.printText()

	Task.detached {
		for _ in 0..<50 {
			try? await Task.sleep(nanoseconds: 1_000_000_000 / 100)
			print("count1 : \(await counter.increase())")
		}
	}
	Task.detached {
		for _ in 0..<50 {
			try? await Task.sleep(nanoseconds: 1_000_000_000 / 100)
			print("count2 : \(await counter.increase())")
		}
	}
}

Actor 사용 결과

이렇게 Actor를 사용해서 Data race를 방지하면서 Task 간 안전하게 가변 데이터를 공유할 수 있습니다.


GlobalActor란?

  • Actor는 하나의 인스턴스에 대해서 격리 영역을 만든다.
  • GlobalActor는 globally-unique actor로 actor의 격리 영역을 global 하게 확장시킨 개념
    GlobalActor에 격리시키고 싶은 인터페이스 앞에 해당 Actor를 붙여서 사용한다.
  • OS에서 제공해 주고 있는 대표적인 GlobalActor가 MainActor UIKit의 모든 타입이 MainActor로 선언되어 있다.


참고 자료

https://sujinnaljin.medium.com/swift-actor-뿌시기-249aee2b732d
https://medium.com/hcleedev/swift-actor란-f8f58c68dab9
https://insubkim.tistory.com/437
https://zeddios.tistory.com/1303
https://zeddios.tistory.com/1290
https://green1229.tistory.com/341

profile
🍎 iOS Developer

0개의 댓글