Main-Actor--Global-Actor
Main Actor
- 메인스레드에 너무 많은 작업을 처리하게 되면 hang이 발생할 수 있기에 이때까지는 백그라운드 스레드에서 돌리고 메인스레드에서 해야하는 것만 DispatchQueue.main.async로 UI를 업데이트했었음
그래서 나온 개념이 바로 MainActor!
- 메인 스레드에서 동작하는 global actor임!
actor Actor {
let id = UUID().uuidString
// property 에 붙으면, 해당 property 는 main thread 에서만 접근 가능
@MainActor var myProperty: String
init(_ myProperty: String) {
self.myProperty = myProperty
}
// method 에 붙으면, 해당 메서드는 main thread 에서만 호출 가능
@MainActor func changeMyProperty(to newValue: String) {
self.myProperty = newValue
}
func changePropertyToName() {
Task { @MainActor in
// block 안에 들어있는 코드도 main actor 에서 실행
myProperty = "sunny"
}
}
}
- MainActor는 자동으로 UI관련 API(UIView, UIViewController 내 정의된 기본 API)가 메인 스레드에서 적절하게 디스패치 되도록 제공해주는 속성
- 즉, Swift의 Concurrency를 사용하고 MainActor가 표시된 컨텍스트 내에서 비동기 코드를 작성하면 실수로 백그라운드 큐에서 UI 업데이트를 해주는 오류에 대해 더이상 걱정할 필요가 x
- 하지만 여전히 main Actor도 actor니까 너무 오래걸리는 작업은 actor나 detached task에 넣어줘야 함
- 기본적인 view들은 MainActor가 이미 붙여져 있는 상태
- UILabel, UIViewController 등등
- 단, Swift Concurrency를 사용할 때만 @MainActor가 적용되고 completion handler나 combine을 사용하면 아무 소용 x
Q. main Actor가 global actor인 이유?
A. UI는 무조건 메인 스레드에서만 실행되어야 하기 때문에 글로벌 actor임
- shared를 사용하는 싱글톤 인스턴스로 사용해야 함
- 그렇다고 해서 객체가 싱글톤인 게 아니고 UI요소를 업데이트를 공유된 컨테스트를 가지고 메인스레드에서만 업데이트되도록 한다는 의미임 헷갈리지 말 것!
- 즉 Swift의 Concurrency를 사용할 때 해당 클래스의 모든 속성과 메서드가 메인 큐에서 자동으로 설정 및 호출/접근 된다는것을 의미
Global Actor
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public protocol GlobalActor {
/// The type of the shared actor instance that will be used to provide
/// mutually-exclusive access to declarations annotated with the given global
/// actor type.
associatedtype ActorType : Actor
/// The shared actor instance that will be used to provide mutually-exclusive
/// access to declarations annotated with the given global actor type.
///
/// The value of this property must always evaluate to the same actor
/// instance.
static var shared: Self.ActorType { get }
/// The shared executor instance that will be used to provide
/// mutually-exclusive access for the global actor.
///
/// The value of this property must be equivalent to `shared.unownedExecutor`.
static var sharedUnownedExecutor: UnownedSerialExecutor { get }
}
- represent a globally unique actor that anywhere in the program
- shared를 사용하는 싱글톤 인스턴스로 사용해야 함
@globalActor
public struct SomeGlobalActor {
public actor MyActor { }
public static let shared = MyActor()
}
MainActor VS DispatchQueue
Task { @MainActor in // 해당 안의 코드가 모두 메인스레드에서 동작하게 됨
self.isUnlocked = true
}
DispatchQueue.main.async {
self.isUnlocked = true
}
두 코드의 차이점이 뭘까?
- 위의 코드는 concurrency 모델을 사용한 것으로 해당 블록이 메인 스레드에서만 호출되도록 컴파일 타임에 보장
- 아래 코드는 GCD의 기능을 사용하여 메인 큐에 비동기적으로 보냄. 런타임 타임에 해당 작업이 메인 큐에서 실행됨을 보장
Task { @MainActor in
...
}
await MainActor.run {
...
}
- 위의 두 코드의 차이점
- 위의 코드는 async한 함수를 호출할 수 있으나 아래의 코드에서는 sync함수만 호출 가능
Actor vs Global Actor
- 둘다 동시성 제어를 하지만 actor는 해당 인스턴스에 대해서 task들 관리를 진행
- global actor는 해당 타입에 대해서 task 관리함
import Foundation
actor Counter {
var value = 0
let name: String
nonisolated var computedProperty: String {
return "Counter 이름: \(name)"
}
var computedProperty2: String {
return "Counter2 이름: \(name)"
}
init(name: String) {
self.name = name
}
func increment() -> Int {
value += 1
return value
}
}
Task.detached {
let counter = Counter(name: "그냥 counter")
print(counter.name)
print(await counter.increment())
}
Task.detached {
let counter = Counter(name: "그냥 counter")
print(counter.name)
print(await counter.increment())
}
/*
그냥 counter
그냥 counter
1
1
*/
import Foundation
@globalActor actor GlobalCounter {
static var shared = GlobalCounter(name: "sunny")
var value = 0
let name: String
nonisolated var computedProperty: String {
return "Counter 이름: \(name)"
}
var computedProperty2: String {
return "Counter2 이름: \(name)"
}
init(name: String) {
self.name = name
}
func increment() -> Int {
value += 1
return value
}
}
Task.detached {
let counter = GlobalCounter.shared
print(counter.name)
print(await counter.increment())
}
Task.detached {
let counter = GlobalCounter.shared
print(counter.name)
print(await counter.increment())
}
/*
sunny
sunny
1
2
*/
→ 따라서 전역적으로 동시성을 제어를 하는 경우에 globalActor를 사용
- 특정 작업이 특정 스레드에서만 실행되어야 하는 경우
- UI 업데이트는 메인 스레드에서만 수행해야 하므로, UI 업데이트와 관련된 코드를 메인 스레드에서 실행하기 위해 사용
- 특정 자원에 대한 접근 제어
- 데이터베이스 또는 파일 시스템과 같은 자원에 대한 액세스를 제한하려는 경우, 해당 자원에 대한 모든 액세스를 특정 actor에서만 수행하도록 사용
- 전역적인 동시성 관리
- 프로그램 전체에서 특정 작업에 대한 동시성을 보장하려는 경우
결론
- global actor는 싱글톤으로 구현되며 특정 액터를 글로벌 스레드를 통해서 사용할 수 있는 방법
- actor는 특정 객체의 동시성을 관리하고, globalActor는 특정 타입의 전역 동시성을 관리
- globalActor는 앱의 전반적인 동작을 제어하고, actor는 객체 내의 상태와 동작을 보호
- mainActor는 global actor의 하나로 UI업데이트를 무조건적으로 main thread에서 실행되어야하는 코드에 적용
참고
swift-evolution/proposals/0316-global-actors.md at main · apple/swift-evolution