class Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
asyncDetached {
print(counter.increment())
}
asyncDetached {
print(counter.increment())
}
var array1 = [1, 2]
var array2 = array1
array1.append(3)
array2.append(4)
print(array1) // [1,2,3]
print(array2) // [1,2,4]
struct Counter {
var value = 0
mutating func increment() -> Int {
value = value + 1
return value
}
}
var counter = Counter()
asyncDetached {
print(counter.increment()) // error!
}
asyncDetached {
print(counter.increment()) // error!
}
복사본을 만드는 방법도 있지만, 이건 원하는 게 아니다. 즉, shared mutable state는 어쩔 수 없이 필요한 부분이 있다.
shared mutable state는 synchronization이 필요하다.
위 도구들은 각자의 장점을 가지고 있지만, 결국 원칙대로 쓰도록 주의하지 않으면 문제가 일어날 소지가 있다는 결정적인 약점이 있다.
이 문제를 해결하기 위해 Actor를 도입했다.
Actor Type
actor Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
var counter = Counter()
asyncDetached {
print(await counter.increment())
}
asyncDetached {
print(await counter.increment())
}
extension Counter {
func resetSlowly(to newValue: Int) {
value = 0
for _ in 0..<newValue {
increment()
}
assert(value == newValue)
}
}
actor ImageDownloader {
// actor 내에서 사용되기 때문에 데이터 경합 문제에서 자유롭다.
private var cache: [URL: Image] = [:]
func image(from url: URL) async throws -> Image? {
if let cached = cache[url] {
return cached
}
// 여기서 작업이 끝날때까지 멈추고, 다른 작업이 수행된다.
// 이 때 이 메소드가 한번 더 호출되면, cache는 여전히 빈 상태라 요청이 한번더 가게 된다.
// 만약 이 때 서버에서 이미지를 갈아끼웠다면, 어떤게 먼저 끝날 지 알 수 없다.
let image = try await downloadImage(from: url)
// cache[url] = image
cache[url] = cache[url, default: image] // 먼저 들어간 이미지를 보전한다.
return image
}
}
Actor isolation → 코드가 actor 안에서 호출되는가 바깥에서 호출되는가?
actor도 프로토콜 채택이 가능하다. 다만 격리를 보장할 수 없다고 판단되면 컴파일러가 오류를 낸다.
actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Bool] = []
}
extension LibraryAccount: Equatable {
static func ==(_ lhs: LibraryAccount, _ rhs: LibraryAccount) {
return lhs.idNumber == rhs.idNumber // OK
}
}
extension LibraryAccount: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(idNumber) // error: actor-isolated method hash(into:) cannot satisfy synchronous requirement
}
}
이럴때는 nonisolated 키워드를 붙여서 actor의 메소드지만, 격리를 하지 않도록 해줄 수 있다.
extension LibraryAccount: Hashable {
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(idNumber)
}
}
서로 다른 액터끼리 공유해도 괜찮은 타입을 나타내기 위한 프로토콜
가능한 타입 → 마킹용 프로토콜이라서, 구현 요구사항은 없다. 스스로 판단해야 한다.
Swift는 Sendable이 아닌 타입이 공유되는 것을 컴파일러에서 막아준다.
Sendable 채택
그동안 써왔던 asyncDetached도 sendable을 사용하고 있다.
func asyncDetached<T>(_ operation: @Sendable () async -> T) -> Task.Handle<T, Never>
MainActor