Protect mutable state with Swift actors

아론·2021년 7월 28일
2

WWDC

목록 보기
1/1

WWDC2021 - Protect mutable state with Swift actors 시청 후 정리한 글입니다.

Summary

서로 다른 스레드가 가변 상태에 동시 접근할 경우 경쟁 조건이 발생할 수 있습니다.
Swift Actor를 통해 가변 상태를 보호하는 방법에 대해 알아봅니다.

Data races make concurrency hard

서로 다른 스레드가 가변 상태에 동시 접근할 때, 그 중 하나 이상의 스레드가 쓰기 작업인 경우 경쟁 조건이 발생합니다.

데이터 경쟁이 발생하는 코드는 작성하기 쉽지만 디버깅하기 어렵기로 악명이 높습니다.

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)로 인해 발생합니다.

데이터가 불변 상태이거나 여러 작업에서 동시에 공유되지 않으면 데이터 경쟁 문제가 발생하지 않습니다.

Value semantics help eliminate data races

데이터 경쟁을 피하는 방법 중 하나는 값 타입을 사용해서 가변 공유 상태를 제거하는 것입니다.

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())
}

위 문제는 여전히 가변 공유 상태가 필요한 경우가 있다는 것을 우리에게 알려줍니다.

Shared mutable state in concurrent programs

우리는 데이터 경쟁을 피하기 위해 특정 형태의 동기화가 필요합니다.

아래와 같은 방법들을 통해 해결할 수 있지만, 사용자의 주의가 필요합니다.

  • Atomics
  • Locks
  • Serial dispatch queues

Swift는 보다 쉽게 데이터 경쟁을 해결하기 위해 액터를 도입합니다.


Actors

액터는 가변 공유 상태를 위한 동기화 메커니즘입니다.

액터의 경우 Swift가 기본적으로 Lock, Serial dispatch queues와 같은 상호 배제 속성을 보장합니다.

Actor types

액터는 아래와 같은 특징을 가지고 있습니다.

  • 액터는 자체적으로 상태를 가지고 있으며 해당 상태는 프로그램의 나머지 부분과 분리되어 있습니다.
  • 해당 상태에 접근하는 유일한 방법은 액터를 통과하는 것(by going through the actor)입니다.
  • 액터를 통과할 때마다 액터의 동기화 메커니즘은 다른 코드가 상태에 접근하지 못하도록 합니다.
  • Struct, Enum, Class 와 비슷한 특성을 가집니다.
  • 참조 타입입니다.
actor Counter {
	var value = 0

	func increment() -> Int {
		value = value + 1
		return value
	}
}

Actor isolation prevents unsynchronized access

액터의 내부 동기화가 가변 공유 상태에 대한 데이터 경쟁을 없앴기 때문에 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'
}

Asynchronous interaction with actors

액터는 외부와 비동기적으로 상호 작용합니다.

액터가 사용중이면 실행중인 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)

Synchronous interaction within an actor

Counter의 익스텐션에 resetSlowly(to:) 함수를 정의했습니다.

함수는 액터 내부에 있기 때문에 상태에 직접 접근할 수 있으며, increment()와 같은 다른 메서드를 동기적으로 호출할 수 있습니다.

액터에서 실행 중이라는 것을 알고 있기 때문에 기다릴 필요가 없습니다.

extension Counter {
	func resetSlowly(to newValue: Int) {
		value = 0
		for _ in 0..<newValue {
			increment()
		}
		assert(value == newValue)
	}
}

Actor reentrancy

액터의 동기 코드는 중단 없이 완료될 때까지 실행됩니다.

시스템의 다른 비동기 코드와 상호 작용할 경우에 대해 알아봅시다.

💡재진입성(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?
두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태를 말한다.


Actor isolation

이번 섹션은 프로토콜 준수, 클로저 및 클래스를 비롯한 언어 내 다른 기능과 상호 작용하는 방식에 대해 설명합니다.

Protocol conformace

다른 타입과 마찬가지로 액터는 프로토콜을 따를 수 있습니다.

LibraryAccount 액터가 Equtable 프로토콜을 따르는 예제입니다.

actor LibraryAccount {
	let idNumber: Int
	var booksOnLoan: [Book] = []
}

extension LibraryAccount: Equatable {
	static func ==(lhs: LibraryAccount, rhs: LibraryAccount) -> Bool {
		lhs.idNumber == rhs.idNumber
	}
}

정적 메서드로 자체 인스턴스가 없으므로 액터와 격리되지 않습니다.

파라미터로 넘어온 액터 타입의 경우 불변 상태에만 접근하기 때문에 아무런 문제없는 코드입니다.

Conformances must respect actor isolation

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)

이 문제를 해결하기 위에 메서드를 비격리 상태로 만들 수 있습니다.

Protocol conformances and non-isolated declearations

해당 메서드가 액터 외부에 있는 것으로 취급하기 위해 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에 접근하기 때문에 문제없이 동작합니다.

Non-isolated declarations are outside the actor

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
    }
}

Closures can be isolated to the actor

클로저는 함수와 마찬가지로 액터와 격리되거나 격리되지 않을 수 있습니다.

아래는 대출한 책의 일부를 읽고 읽은 총 페이지 수를 반환하는 예제입니다.

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)!

Passing data into and out of actors

액터 내 값 타입의 인스턴스에 대해 액터 외부에서 데이터를 패싱하는 것은 괜찮습니다.

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 types

Sendable은 다른 액터 간에 값을 공유할 수 있는 타입입니다.

  • 값 타입의 경우 복사본이 독립적이기 때문에 Sendable입니다.

  • 액터는 가변 상태 접근에 대해 동기화하기 때문에 Sendable입니다.

  • 자신과 모든 하위 클래스가 변경할 수 없는 데이터만 가진 클래스의 경우 Sendable이 될 수 있습니다. (Immutable Classes)

  • 내부적으로 동기화(예: Lock)를 수행하여 안전한 접근이 보장되는 클래스의 경우 Sendable이 될 수 있습니다. (Internally-synchronized class)

Checking Sendable

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)!!!"
}

Adoptinng Sendable

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 functions

@Sendable 함수 타입Sendable프로토콜을 준수합니다.

@Sendable은 클로저에 대해 아래와 같은 제약을 두었습니다.

  • 가변 상태를 캡처할 수 없습니다. (No mutable captures)
  • 캡처 대상은 Sendable타입입니다. (Captures must be of Sendable type)
  • 동기식 Sendable 클로저는 외부에서 코드를 실행할 수 있기 때문에 액터를 격리할 수 없습니다. (Cannot be both synchronous and actor-isolated)
func asyncDetached<T>(_ operation: @Sendable () async -> T) -> Task.Handle<T. Never>

@Sendable closure restriction

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'
        }
    }
}

Main Actor

메인 스레드에서 동작을 보장하는 액터입니다.

Interacting with the main thread

메인 스레드는 UI 렌더링, 사용자와 상호 작용하는 이벤트 등 중요한 작업들을 처리합니다.

모든 작업을 메인 스레드에서 처리할 경우 UI가 멈추는 등의 문제가 발생합니다.

그렇기 때문에 우리는 입/출력 작업이 느리거나, 네트워크 통신 등 시간이 오래 걸리는 작업을 다른 스레드에서 처리하고, 필요한 경우 DisPatchQueue.main.async를 통해 메인 스레드에서 실행할 작업을 작성합니다.

func checkedOut(_ booksOnLoan:[Book]) {
	booksView.checkedOutBooks = booksOnLoan
}

DispatchQueue.main.async {
	checkedOut(booksOnLoan)
}

The main Actor

메인 스레드에서 실행을 보장하는 특별한 메인 액터가 있습니다.

일반 액터와 두 가지 차이점이 있습니다.

  1. 메인 액터는 디스패치 큐를 통해 모든 동기화를 수행합니다. 즉, 런타임 관점에서 DispatchQueue.main로 대체해서 사용할 수 있습니다.
  2. 메인 스레드에 있어야 할 코드와 데이터가 여기저기 흩어져 있습니다. (SwiftUI, AppKit, UIKit, 기타 시스템 프레임워크)

Swift concurrency를 사용하면 @MainActor를 표시하여 메인 액터에서 실행되어야 한다고 작업할 수 있습니다.

@MainActor func checkedOut(_ booksOnLoan:[Book]) {
	booksView.checkedOutBooks = booksOnLoan
}

await checkedOut(booksOnLoan)

메인 액터 외부에서 호출하는 경우 대기해야 하므로 메인 스레드에서 비동기적으로 수행됩니다.

메인 스레드에서 실행할 코드들을 @MainActor로 표시하면 DispatchQueue.main을 언제 사용해야 할 지 고민할 필요가 없습니다.

Swift는 해당 코드들을 항상 메인 스레드에서 실행되도록 합니다.

Main actor types

타입도 @MainActor로 정의하여 모든 멤버, 하위 클래스들을 메인 액터에 있게 할 수 있습니다.

메인 스레드에서 실행되어야 하는 UI와 상호 작용하는 코드들에서 자주 사용합니다.

메소드는 개별적으로 nonisolated를 통해 opt-out 할 수 있습니다.

@MainActor class MyViewController: UIViewController {
	func onPress(...) { ... } // implicitly @MainActor

	nonisolated func fetchLatestAndDisplay() async { ... }
}

Actors


참고


수정이 필요한 부분 있으면 피드백 부탁드립니다. 😁

profile
😁

0개의 댓글