WWDC2021 - Protect mutable state with Swift actors 시청 후 정리한 글입니다.
서로 다른 스레드가 가변 상태에 동시 접근할 경우 경쟁 조건이 발생할 수 있습니다.
Swift Actor를 통해 가변 상태를 보호하는 방법에 대해 알아봅니다.
서로 다른 스레드가 가변 상태에 동시 접근할 때, 그 중 하나 이상의 스레드가 쓰기 작업인 경우 경쟁 조건이 발생합니다.
데이터 경쟁이 발생하는 코드는 작성하기 쉽지만 디버깅하기 어렵기로 악명이 높습니다.
class Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
asyncDetached {
print(counter.increment())
}
asyncDetached {
print(counter.increment())
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
class Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
asyncDetached { // Warning: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
print(counter.increment())
}
asyncDetached { // Warning: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
print(counter.increment())
}
위 예제의 경우 실행 시점에 따라 결과값( 1, 2
or 2, 1
or 1, 1
or 2, 2
)이 달라지며, 의도하지 않은 잘못된 값이 발생할 수 있습니다.
데이터 경쟁은 가변 공유 상태(shared mutable state)로 인해 발생합니다.
데이터가 불변 상태이거나 여러 작업에서 동시에 공유되지 않으면 데이터 경쟁 문제가 발생하지 않습니다.
데이터 경쟁을 피하는 방법 중 하나는 값 타입을 사용해서 가변 공유 상태를 제거하는 것입니다.
var array1 = [1, 2]
var array2 = array1
array1.append(3)
array2.append(4)
print(array1) // [1, 2, 3]
print(array2) // [1, 2, 4]
데이터 경쟁을 피하기 위해 counter
를 값 타입으로 변경하지만, 상수이기 때문에 컴파일 오류가 발생합니다. (⚠️ Cannot use mutating member on immutable value: 'counter' is 'let' constant
)
struct Counter {
var value = 0
mutating func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
asyncDetached {
print(counter.increment()) // Error: Cannot use mutating member on immutable value: 'counter' is 'let' constant
}
asyncDetached {
print(counter.increment()) // Error: Cannot use mutating member on immutable value: 'counter' is 'let' constant
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
struct Counter {
var value = 0
mutating func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
asyncDetached {
print(counter.increment()) // Error: Cannot use mutating member on immutable value: 'counter' is a 'let' constant Change 'let' to 'var' to make it mutable
}
asyncDetached {
print(counter.increment()) // Error: Cannot use mutating member on immutable value: 'counter' is a 'let' constant Change 'let' to 'var' to make it mutable
}
컴파일 오류를 해결하기 위해counter
를 변수로 만들면 될 것 같지만, 이는 다시 데이터 경쟁 문제가 발생합니다.
다행히 컴파일러는 아래와 같은 안전하지 않는 코드를 허용하지 않습니다. (⚠️ Mutation of captured var 'counter' in concurrently-executing code
)
struct Counter {
var value = 0
mutating func increment() -> Int {
value = value + 1
return value
}
}
var counter = Counter()
asyncDetached {
print(counter.increment()) // Error: Mutation of captured var 'counter' in concurrently-executing code
}
asyncDetached {
print(counter.increment()) // Error: Mutation of captured var 'counter' in concurrently-executing code
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
실습간 컴파일 오류는 발생하지 않았습니다.
struct Counter {
var value = 0
mutating func increment() -> Int {
value = value + 1
return value
}
}
var counter = Counter()
asyncDetached { // Warning: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
print(counter.increment())
}
asyncDetached { // Warning: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
print(counter.increment())
}
지역 변수를 통해 컴파일 오류를 해결할 수 있지만, 해당 코드는 잘못된 결과 값을 반환합니다.
struct Counter {
var value = 0
mutating func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
asyncDetached {
var counter = counter
print(counter.increment())
}
asyncDetached {
var counter = counter
print(counter.increment())
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
struct Counter {
var value = 0
mutating func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
asyncDetached { // Warning: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
var counter = counter
print(counter.increment())
}
asyncDetached { // Warning: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
var counter = counter
print(counter.increment())
}
위 문제는 여전히 가변 공유 상태가 필요한 경우가 있다는 것을 우리에게 알려줍니다.
우리는 데이터 경쟁을 피하기 위해 특정 형태의 동기화가 필요합니다.
아래와 같은 방법들을 통해 해결할 수 있지만, 사용자의 주의가 필요합니다.
Swift는 보다 쉽게 데이터 경쟁을 해결하기 위해 액터를 도입합니다.
액터는 가변 공유 상태를 위한 동기화 메커니즘입니다.
액터의 경우 Swift가 기본적으로 Lock, Serial dispatch queues와 같은 상호 배제 속성을 보장합니다.
액터는 아래와 같은 특징을 가지고 있습니다.
actor Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
액터의 내부 동기화가 가변 공유 상태에 대한 데이터 경쟁을 없앴기 때문에 1, 2
또는 2, 1
과 같은 유효한 결과만 얻을 수 있습니다.
actor Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
asyncDetached {
print(counter.increment())
}
asyncDetached {
print(counter.increment())
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
actor Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
asyncDetached { // Warining: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
print(counter.increment()) // Error: Expression is 'async' but is not marked with 'await'
}
asyncDetached { // Warining: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
print(counter.increment()) // Error: Expression is 'async' but is not marked with 'await'
}
액터는 외부와 비동기적으로 상호 작용합니다.
액터가 사용중이면 실행중인 CPU가 다른 작업을 수행할 수 있도록 코드를 중단합니다.
다시 자유를 얻게되면 중단된 코드를 재개합니다.
await
키워드를 통해 액터에 대한 비동기 호출에 일시 중단이 포함될 수 있음을 나타냅니다.
actor Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
asyncDetached {
print(await counter.increment())
}
asyncDetached {
print(await counter.increment())
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
actor Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
asyncDetached { // Warining: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
await print(counter.increment())
}
asyncDetached { // Warining: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
await print(counter.increment())
}
⚠️ 해당 예제의 경우 실습 환경에서 동작하지 않습니다. (Error: The LLDB RPC server has crashed)
Counter
의 익스텐션에 resetSlowly(to:)
함수를 정의했습니다.
함수는 액터 내부에 있기 때문에 상태에 직접 접근할 수 있으며, increment()
와 같은 다른 메서드를 동기적으로 호출할 수 있습니다.
액터에서 실행 중이라는 것을 알고 있기 때문에 기다릴 필요가 없습니다.
extension Counter {
func resetSlowly(to newValue: Int) {
value = 0
for _ in 0..<newValue {
increment()
}
assert(value == newValue)
}
}
액터의 동기 코드는 중단 없이 완료될 때까지 실행됩니다.
시스템의 다른 비동기 코드와 상호 작용할 경우에 대해 알아봅시다.
💡재진입성(Reetrancy)?
컴퓨터 프로그램 또는 서브루틴에 재진입성이 있으면, 이 서브루틴은 동시에(병렬) 안전하게 실행 가능합니다. 즉 재진입이 가능한 루틴은 동시에 접근해도 언제나 같은 실행 결과를 보장합니다.
여러 '사용자/객체/프로세스'와 멀티프로세싱이 대개 재진입 코드의 제어를 복잡하게 만듭니다. 또한 입출력 코드는 디스크나 터미널과 같은 공유 자원에 의존하고 있기 때문에 보통은 재진입성이 없습니다.
재진입성은 함수형 프로그래밍의 핵심 개념입니다.
이미지 다운로더 액터가 있습니다.
이미지 다운로더는 다른 서비스에서 이미지를 다운받는 역할을 수행하며, 다운 받은 이미지를 캐시에 저장하여 동일한 이미지에 대한 중복 다운로드를 방지합니다.
actor ImageDownloader {
private var cache: [URL: Image] = [:]
func image(from url: URL) async throws -> Image? {
if let cached = cache[url] {
return cached
}
let image = try await downloadImage(from: url)
cache[url] = image
return image
}
}
액터의 동기화 메커니즘은 하나의 작업만 캐시에 접근할 수 있도록 보장합니다.
이 때 동일한 이미지를 가져오려고 하는 두 개의 동시 작업(Task1
, Task2
)이 있다고 가정해봅시다.
Task1
은 캐시가 없음을 확인하고 서버에 이미지 다운로드 요청합니다.
Task1
작업이 이미지를 다운로드하는 동안 동일한 URL에 새로운 이미지가 배포되었습니다.
새로 배포된 이미지를 Task2
에서 가져올 경우 Task1
에서 가져온 이미지를 덮어쓰는 오류가 발생합니다.
오류를 해결하기 위해 실행을 재개할 때 캐시에 이미 항목이 있는 경우 원래 버전을 유지하도록 예외처리합니다.
더 나은 해결 방법은 중복 다운로드를 완전히 피하는 것입니다.
액터 재진입(Actor Reentrancy)은 교착 상태(deadlock)를 방지하고 진행을 보장하지만, await
에서 가정(assumption)을 확인해야 합니다.
전역 상태, 시계, 타이머, 액터에 대한 가정은 await
이후 확인이 필요합니다.
재진입을 잘 설계하려면 액터 상태 변경은 동기 코드에서 수행하도록 합니다.
💡Deadlock?
두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태를 말한다.
이번 섹션은 프로토콜 준수, 클로저 및 클래스를 비롯한 언어 내 다른 기능과 상호 작용하는 방식에 대해 설명합니다.
다른 타입과 마찬가지로 액터는 프로토콜을 따를 수 있습니다.
LibraryAccount
액터가 Equtable
프로토콜을 따르는 예제입니다.
actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
}
extension LibraryAccount: Equatable {
static func ==(lhs: LibraryAccount, rhs: LibraryAccount) -> Bool {
lhs.idNumber == rhs.idNumber
}
}
정적 메서드로 자체 인스턴스가 없으므로 액터와 격리되지 않습니다.
파라미터로 넘어온 액터 타입의 경우 불변 상태에만 접근하기 때문에 아무런 문제없는 코드입니다.
LibraryAccount
액터가 Hashable
프로토콜을 따르는 예제입니다.
actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
}
extension LibraryAccount: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(idNumber) // Error: actor-isolated method 'hash(into:)' cannot satisfy synchronous requirement
}
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
actor LibraryAccount {
let idNumber: Int = 0
var booksOnLoan: [Book] = []
}
extension LibraryAccount: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(idNumber) // Error: Actor-isolated instance method 'hash(into:)' cannot be used to satisfy a protocol requirement
}
}
hash(into:)
함수는 외부에서 호출될 수 있는데, 비동기 호출이 아니므로 액터 격리를 유지할 수 없다고 오류가 발생합니다. (⚠️ actor-isolated method 'hash(into:)' cannot satisfy synchronous requirement
)
이 문제를 해결하기 위에 메서드를 비격리 상태로 만들 수 있습니다.
해당 메서드가 액터 외부에 있는 것으로 취급하기 위해 nonisolated
를 사용합니다.
actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
}
extension LibraryAccount: Hashable {
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(idNumber)
}
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
actor LibraryAccount {
let idNumber: Int = 0
var booksOnLoan: [Book] = []
nonisolated var hashValue: Int { // `nonisolated` 미작성시 Error: Actor-isolated property 'hashValue' cannot be used to satisfy a protocol requirement
idNumber
}
}
extension LibraryAccount: Hashable {
static func ==(lhs: LibraryAccount, rhs: LibraryAccount) -> Bool {
lhs.idNumber == rhs.idNumber
}
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(idNumber)
}
}
예제 코드의 경우 불변 상태인 idNumber
에 접근하기 때문에 문제없이 동작합니다.
nonisolated
의 경우 액터 외부에 있는 것과 동일하게 취급되기 때문에 가변 상태를 참조할 수 없습니다.
만약 booksOnLoan
에 접근할 경우 컴파일 오류가 발생합니다. (⚠️ actor-isolated 'booksOnLoan' cannot be referenced from outside the actor
)
actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
}
extension LibraryAccount: Hashable {
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(booksOnLoan) // Error: actor-isolated 'booksOnLoan' cannot be referenced from outside the actor
}
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
actor LibraryAccount {
let idNumber: Int = 0
var booksOnLoan: [Book] = []
nonisolated var hashValue: Int {
idNumber
}
}
extension LibraryAccount: Hashable {
static func ==(lhs: LibraryAccount, rhs: LibraryAccount) -> Bool {
lhs.idNumber == rhs.idNumber
}
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(booksOnLoan) // Error: Actor-isolated property 'booksOnLoan' can not be referenced from a non-isolated context
}
}
클로저는 함수와 마찬가지로 액터와 격리되거나 격리되지 않을 수 있습니다.
아래는 대출한 책의 일부를 읽고 읽은 총 페이지 수를 반환하는 예제입니다.
extension LibraryAccount {
func readSome(_ book: Book) -> Int {
// ...
}
func read() -> Int {
booksOnLoan.reduce(0) { book in
readSome(book)
}
}
}
위 예제는 reduce 작업이 동기적으로 실행되고, 동시 접근을 유발할 수 있는 다른 스레드로 클로저가 벗어날 수 없기 때문에 안전한 코드입니다.
asyncDetached
는 액터가 수행하는 다른 작업과 동시에 클로저를 실행합니다.
클로저는 액터 외부에 있으므로 await
를 사용합니다.
extension LibraryAccount {
func read() -> Int {
// ...
}
func readLater() {
asyncDetached {
await read()
}
}
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
extension LibraryAccount {
func read() -> Int {
return 0
}
func readLater() {
asyncDetached { // Warning: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
await read()
}
}
}
⚠️ 해당 예제의 경우 실습 환경에서 동작하지 않습니다. (Error: The LLDB RPC server has crashed)!
액터 내 값 타입의 인스턴스에 대해 액터 외부에서 데이터를 패싱하는 것은 괜찮습니다.
actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
func selectRandomBook() -> Book? { }
}
struct Book {
var title: String
var authors: [Author]
}
func visit(_ account: LibraryAccount) async {
guard var book = await account.selectRandomBook() else { return }
book.title = "\(book.title)!!!"
}
이번엔 값 타입을 클래스로 변경해보겠습니다.
visit(:)
함수 내부에 책에 대한 참조가 생성되어, 액터 외부에서 가변 공유 상태에 대해 변경할 수 있게 되었습니다. (⚠️ 데이터 경쟁 발생)
actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
func selectRandomBook() -> Book? { }
}
class Book {
var title: String
var authors: [Author]
}
func visit(_ account: LibraryAccount) async {
guard var book = await account.selectRandomBook() else { return }
book.title = "\(book.title)!!!"
}
값 타입과 액터를 함께 사용하는 것은 안전하지만 클래스는 문제를 야기할 수 있습니다.
이를 해결하기 위해 Sendable
타입을 사용합니다.
Sendable
은 다른 액터 간에 값을 공유할 수 있는 타입입니다.
값 타입의 경우 복사본이 독립적이기 때문에 Sendable
입니다.
액터는 가변 상태 접근에 대해 동기화하기 때문에 Sendable
입니다.
자신과 모든 하위 클래스가 변경할 수 없는 데이터만 가진 클래스의 경우 Sendable
이 될 수 있습니다. (Immutable Classes)
내부적으로 동기화(예: Lock)를 수행하여 안전한 접근이 보장되는 클래스의 경우 Sendable
이 될 수 있습니다. (Internally-synchronized class)
Sendable
타입은 데이터 경쟁으로부터 코드를 보호합니다.
non-Sendable 타입의 경우 Swift가 정적으로 검사하여 오류를 발생합니다. (⚠️ call to actor-isolated method 'selectRandomBook' returns non-Sendable type 'Book?'
)
actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
func selectRandomBook() -> Book? { }
}
class Book {
var title: String
var authors: [Author]
}
func visit(_ account: LibraryAccount) async {
guard var book = await account.selectRandomBook() else { return } // Error: call to actor-isolated method 'selectRandomBook' returns non-Sendable type 'Book?'
book.title = "\(book.title)!!!"
}
Book
구조체의 모든 속성이 Sendable
인 경우 Book
구조체는 Sendable
이 될 수 있습니다.
Sendable
이 아닌 속성을 가진 경우 컴파일 오류가 발생합니다. (⚠️ Sendable type 'Book' has non-Sendable stored property 'authors' of type '[Author]'
)
struct Book: Sendable {
var title: String
var authors: [Author] // Error: Sendable type 'Book' has non-Sendable stored property 'authors' of type '[Author]'
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
struct Book: Sendable {
var title: String
var authors: [Author] // Error: Stored property 'authors' of 'Sendable'-conforming struct 'Book' has non-sendable type '[Author]'
}
제네릭의 경우 인자(arguments)에 따라 달라집니다.
struct Pair<T, U> {
var first: T
var second: U
}
extension Pair: Sendable where T: Sendable, U: Sendable {
}
위의 경우 두 인자가 모두 Sendable
인 경우에만 Sendable
이 됩니다.
@Sendable 함수 타입
은 Sendable
프로토콜을 준수합니다.
@Sendable
은 클로저에 대해 아래와 같은 제약을 두었습니다.
Sendable
타입입니다. (Captures must be of Sendable type)func asyncDetached<T>(_ operation: @Sendable () async -> T) -> Task.Handle<T. Never>
Sendable 클로저는 가변 지역 변수에 대한 데이터 경쟁이 발생할 수 있기 때문에 캡처할 수 없습니다. (⚠️ Mutation of captured var 'counter' in concurrently-executing code
)
struct Counter {
var value = 0
mutating func increment() -> Int {
value = value + 1
return value
}
}
var counter = Counter()
asyncDetached {
print(counter.increment()) // Error: Mutation of captured var 'counter' in concurrently-executing code
}
asyncDetached {
print(counter.increment()) // Error: Mutation of captured var 'counter' in concurrently-executing code
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
struct Counter {
var value = 0
mutating func increment() -> Int {
value = value + 1
return value
}
}
var counter = Counter()
asyncDetached { // Warning: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
print(counter.increment())
}
asyncDetached { // Warning: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
print(counter.increment())
}
Sendable 클로저는 액터를 격리할 수 없으므로 비동기여야 합니다. (⚠️ call to asctor-isolated method 'read' must be 'async'
)
extension LibraryAccount {
func read() -> Int { }
func readLater() {
asyncDetached {
read() // Error: call to asctor-isolated method 'read' must be 'async'
}
}
}
아래는 Xcode 13.0 beta 3에서 실습한 코드입니다.
extension LibraryAccount {
func read() -> Int { }
func readLater() {
asyncDetached { // Warning: 'asyncDetached(priority:operation:)' is deprecated: `asyncDetached` was replaced by `Task.detached` and will be removed shortly.
read() // Error: Expression is 'async' but is not marked with 'await'
}
}
}
메인 스레드에서 동작을 보장하는 액터입니다.
메인 스레드는 UI 렌더링, 사용자와 상호 작용하는 이벤트 등 중요한 작업들을 처리합니다.
모든 작업을 메인 스레드에서 처리할 경우 UI가 멈추는 등의 문제가 발생합니다.
그렇기 때문에 우리는 입/출력 작업이 느리거나, 네트워크 통신 등 시간이 오래 걸리는 작업을 다른 스레드에서 처리하고, 필요한 경우 DisPatchQueue.main.async
를 통해 메인 스레드에서 실행할 작업을 작성합니다.
func checkedOut(_ booksOnLoan:[Book]) {
booksView.checkedOutBooks = booksOnLoan
}
DispatchQueue.main.async {
checkedOut(booksOnLoan)
}
메인 스레드에서 실행을 보장하는 특별한 메인 액터가 있습니다.
일반 액터와 두 가지 차이점이 있습니다.
DispatchQueue.main
로 대체해서 사용할 수 있습니다.Swift concurrency를 사용하면 @MainActor
를 표시하여 메인 액터에서 실행되어야 한다고 작업할 수 있습니다.
@MainActor func checkedOut(_ booksOnLoan:[Book]) {
booksView.checkedOutBooks = booksOnLoan
}
await checkedOut(booksOnLoan)
메인 액터 외부에서 호출하는 경우 대기해야 하므로 메인 스레드에서 비동기적으로 수행됩니다.
메인 스레드에서 실행할 코드들을 @MainActor
로 표시하면 DispatchQueue.main
을 언제 사용해야 할 지 고민할 필요가 없습니다.
Swift는 해당 코드들을 항상 메인 스레드에서 실행되도록 합니다.
타입도 @MainActor
로 정의하여 모든 멤버, 하위 클래스들을 메인 액터에 있게 할 수 있습니다.
메인 스레드에서 실행되어야 하는 UI와 상호 작용하는 코드들에서 자주 사용합니다.
메소드는 개별적으로 nonisolated
를 통해 opt-out 할 수 있습니다.
@MainActor class MyViewController: UIViewController {
func onPress(...) { ... } // implicitly @MainActor
nonisolated func fetchLatestAndDisplay() async { ... }
}
수정이 필요한 부분 있으면 피드백 부탁드립니다. 😁