메모리의 스택(stack) 영역은 함수의 호출과 관계되는 지역 변수와 매개변수가 저장되는 영역입니다.
스택 영역은 함수의 호출과 함께 할당되며, 함수의 호출이 완료되면 소멸합니다.
from - https://velog.io/@wansook0316/Swift-ConcurrencyBehind-the-scenes-Part.-01
func updateDatabase(with articles: [Article], for feed: Feed) async throws {
...
try await feed.add(articles)
}
func add(_ newArticles: [Article]) async throws {
let ids = try await database.save(newArticles, for: self)
for (id, article) in zip(ids, newArticles) {
articles[id] = article
}
}
func save(_ newArticles: [Article], for feed: Feed) async throws -> [ID] { /* ... */ }
id
, article
은 await(suspension point)으로 기다릴 필요가 없는 지역변수이므로 stack에 저장한다.newArticles
는 await으로 기다릴 필요가 있는 변수, suspension point를 넘나들어 사용될 수 있는 변수이므로 heap의 async frame
에 저장한다. add
에서 suspension point를 만나 실행이 중지(suspend)되면 곧이어 save
가 실행된다. 그러면 stack의 add
프레임은 pop 되어 교체된다.add
프레임은? pop 되지 않는다.save
가 중지된 후의 상황도 비슷하다. 대신 힙의 여러 continuation, async frame 중 한 개가 쓰레드로 복귀한 후 자신의 suspension point 이후의 일을 계속 할 것이다.save
로 돌아온 뒤하던 일을 마저 하기 시작한다.save
가 반환한 뒤 add
가 실행되고 그 안의 서브루틴zip
이 실행된다. zip
은 새로운 새로운 스택프레임을 만드는 것을 볼 수 있다. 이는 (내 추측인데) zip
이 heap에 프레임을 만들 수 없는 non-async 함수이기 때문이 아닐까?Continuation(연속성)은 CS 개념 중 하나로 프로세스 실행 상태를 가진 자료구조고 런타임에 해제되지 않으며 프로그래밍 언어로 접근할 수 있다.
In computer science, a continuation is an abstract representation of the control state of a computer program. A continuation implements (reifies) the program control state, i.e. the continuation is a data structure that represents the computational process at a given point in the process's execution; the created data structure can be accessed by the programming language, instead of being hidden in the runtime environment.
Continuations are useful for encoding other control mechanisms in programming languages such as exceptions, generators, coroutines, and so on.
class UserStorage {
private var users = [User.ID: User]()
private let queue = DispatchQueue(label: "UserStorage.sync")
func store(_ user: User) {
queue.sync {
self.users[user.id] = user
}
}
func user(withID id: User.ID) -> User? {
queue.sync {
self.users[id]
}
}
}
Data contention
이라고 한단다.class UserStorage {
private var users = [User.ID: User]()
private let queue = DispatchQueue(label: "UserStorage.sync")
func store(_ user: User) {
queue.async {
self.users[user.id] = user
}
}
func loadUser(withID id: User.ID,
handler: @escaping (User?) -> Void) {
queue.async {
handler(self.users[id])
}
}
}
closure
쓰기 귀찮아서라고만 하고 딱히 언급을 안하는데 그건 여기서만든 큐가 serial 큐라서 인 것 같고thread explosion
이 발생하는 문제가 있다.actor UserStorage {
private var users = [User.ID: User]()
func store(_ user: User) {
users[user.id] = user
}
func user(withID id: User.ID) -> User? {
users[id]
}
}
액터는 마치 클래스처럼 동작한다. 다만,
Actor
는 서브클래싱을 할 수 없다. 왜냐면 애초에 클래스가 아니기 때문.final
로 선언해서 바뀔일이 없다고 말해줘야 함. 사용은 이런 식으로 await
을 붙여주면 된다.
class UserLoader {
private let storage: UserStorage
...
func loadUser(withID id: User.ID) async throws -> User {
if let storedUser = await storage.user(withID: id) {
return storedUser
}
let url = URL.forLoadingUser(withID: id)
let (data, _) = try await urlSession.data(from: url)
let user = try decoder.decode(User.self, from: data)
await storage.store(user)
return user
}
}
UserStorage
여러번 사용하면 그건 다시 data race를 만들어버림.
Actor가 async-await
을 위해 만들어졌다기 보다는 Actor
는 그냥 Swift의 새로운 태스크 연속성 관리 단위(프로토콜)고 async
선언 된 인스턴스는 자동으로 Actor
를 따른다고 보는 것이 맞는 듯
수행 도중 중단될 수 없는 하나의 동작 단위.
예를 들어 하나의 프로세서 명령어(instruction)가 수행 중이라면, 어떤 인터럽트가 발생하더라도 그 명령어의 수행은 중단되지 않으며, 프로세서는 그 명령어의 수행이 종료된 이후에 인터럽트를 처리할 것이다. 그러므로, 개별 프로세서 명령어들은 각각 원자적 행위라 할 수 있다. 이 개념은 프로세스 동기화와 상호배제 측면에서 매우 중요하다.
데이터베이스 분야에서의 트랜잭션도 원자적 행위 중의 하나인데, 이는 본래 원자성이 없는 명령어들을 묶어 원자성을 가진 실행 단위로 만든 것이라 할 수 있다. 트랜잭션을 수행하는 도중에 중단 요청이 발생하면 그 종류에 따라 트랙잭션을 모두 수행 후에 그 인터럽트를 처리하거나, 또는 트랜잭션 수행이 시작되기 이전 지점으로 복구한다.
ex. 함수가 atomic하다는 것은 전 영역에 통틀어 1개만 실행되고 있다는 뜻이다.
WWDC에서는 await
주변에서 명백히 Atomicity가 깨지므로 lock을 걸어서는 안된다고 한다. 원자성이 한 명령어가 끝날때까지 수행되는 것이 보장되는 성질이기에 await
에서 원자성이 깨진다는 설명은 이해가 된다. 하지만 lock을 걸어서는 안된다는 것이 대체 무슨 뜻일까.
Lock은 atomicic한 동작을 보장하는 여러 방법 중 하나다. 즉, lock은 thread-safe한 동작을 보장하게끔 추상화된 인터페이스의 구현체이고 그 과정에서 atomic한 연산을 이용했다고 말할 수 있다. 별개의 컨셉이라기보단 방법과 구현의 차이인 것.
그러면 왜 await에서 atomicity가 보장되지 않으므로 lock을 쓰지 말라고 하는 것인지? 한 줄로 요약하면 다른 쓰레드가 lock을 서로 관리하는 과정에서 lock ordering inversion이 발생할 수 있으며 이는 데드락을 발생시킨다. 그러면 또 lock ordering inversion은 무엇일까..
https://softwareengineering.stackexchange.com/questions/425016/what-is-a-lock-ordering-inversion
알아둘 것: lock은 쓰레드 단위로 걸린다.
If Thread 1 seizes lock A, then Thread 2 will wait at Lock A until thread 1 releases it. Whilst Thread 2 waits at Lock A, Thread 1 is free to proceed and also seize Lock B. At some point Thread 1 will release Lock A (either before or after releasing B, it doesn't matter), and Thread 2 will then proceed. If Thread 1 wanted to retake Lock A for whatever reason, then it would only do so after first releasing Lock B. This is the correct ordering, in the sense that it involves no risk of deadlock. A is always taken before B.
In the case of inverted ordering, Thread 1 proposes to seize A before B, whilst Thread 2 proposes to seize B before A. With suitable timing, a situation can come about in which each have seized the first of the locks they proposed to take, yet each also needs access to the other lock in order to proceed. This is a deadlock situation.
기차 선로 비유 : 선로가 2개인 기차역이 있다고 하자. 사고를 미연에 방지하기 위해 각 선로에는 통행을 막는 차단봉이 달려있다. 1번 선로에 2번 선로에 있는 기차가 역을 통과하기 전까지 올라가지 않는 차단봉이 내려왔다. 공교롭게도 2번 선로 역시 1번 선로에 있는 기차가 역을 통과하기 전까지 올라가지 않는 차단봉이 내려왔다. 양 선로의 기차들은 서로가 통과할 때까지 기다리지만 모두 차단봉(락)이 내려와 있으므로 영원히 움직일 수 없다.
이 이유로 2개 이상의 쓰레드에서 서로 락을 관리하는 것은 위험하다. 1번 기차(쓰레드)가 락을 걸고 튀어버리면 그걸 풀 방법이 없기 때문이다.
Swift concurrency는 태스크가 같은 쓰레드에서 실행되는 걸 보장하지 않는다. 그러므로 lock을 금지시키는 것.
Swift concurrency가 런타임에 보장해야 하는 계약같은 것
semaphore.signal()
을 호출하는 것도 불가능하다. OS는 가능한 thread를 탐색한후 데드락을 발생시킨다.