Threading Model
GCD에서는 Queue라는 것을 이용해서 작업을 스케쥴링 한다.
상호 배제를 하기 위해 serialQueue를 사용한다.
병렬적으로 작업을 수행하기위해 concurrent Queue를 사용한다.
let urlSession = URLSession(configuration: .default, delegate: self,
delegateQueue: concurrentQueue)
for feed in feedsToUpdate {
let dataTask = urlSession.dataTask(with: feed.url) { data, response, error in
// 이 부분은 delegateQueue에서 실행된다.
guard let data = data else { return }
do {
let articles = try deserializeArticles(from: data)
databaseQueue.sync {
updateDatabase(with: articles)
}
} catch { ... }
}
}
GCD는 최초에는 코어수만큼 스레드를 사용하다가, 스레드가 블록상태가 되면 이후 작업을 위해 새로운 스레드를 만들어낸다. → 블록된 스레드가 많아질 수록 스레드 폭발 위험이 늘어난다.
Swift는 스레드를 만드는 대신, Continuation이라는 객체를 만들어서, 작업의 실행 과정을 추적한다.
func deserializeArticles(from data: Data) -> [Article]
func updateDatabase(with articles: [Article], for feed: Feed) async
await withThrowingTaskGroup(of: [Article].self) { group in
for feed in feedsToUpdate {
group.async {
let (data, response) = try await URLSession.shared.data(from: feed.url)
let articles = try deserializeArticles(from: data)
await updateDatabase(with: articles, for: feed)
// ...
return aritcles
}
}
}
기본적으로 스레드는 개별적으로 스택을 가진다.
여기에는 함수 호출정보(지역 변수, 복귀 주소, 기타 등등의 정보)가 쌓인다.
async로 선언된 함수는 별도로 heap 영역에 쌓인다.
async 메소드가 메소드 중간에 호출되면, 원래의 스택 프레임은 새로운 함수 호출 프레임으로 대체된다.(이미 힙 영역에 추적해야할 정보는 저장되어 있으니까)
await하고 있는 동안, 해당 스레드는 다른 작업을 할 수 있도록 블록되지 않는다. 관련 정보는 Continuation에 의해 저장되고, 다른 작업이 진행된다.
작업이 끝나면 결과 값을 반환하고, 이후 코드 실행을 이어나간다.
태스크 간 의존성 추척
Runtime Contract
Cooperative thread pool
적용 시 고려 사항
컴파일러 차원에서 지원해주는 await, actor, task group은 상관 없다.
os_unfair_lock, NSLock 등은 제한된 범위에서 짧게 쓰면 괜찮지만, 잘못썼을 때 막아줄 컴파일러 지원이 없기 때문에 주의해야 한다.
DispatchSemaphore나 pthread_cont, NSCondition, pthread_rw_lock 등은 unsafe하다.
// 잘못된 코드
func updateDatabase(_ asyncUpdateDatabase: @Sendable @escaping () async -> Void) {
let semaphore = DispatchSemaphore(value: 0)
async {
await asyncUpdateDatasource()
semaphore.signal()
}
semaphore.wait()
}
runtime Contract가 준수되는지 확인하기 위해서는, 디버그할 때 환경변수에서LIBDISPATCH_COOPERATIVE_POOL_STRICT 값을 1로 주면 된다.
Synchronization
상호 배제
시리얼 큐
액터: 경쟁이 없으면 스레드를 재사용하고, 경쟁이 있어도 스레드를 블록하지 않는다. → 좋은 점만 합침
기존에 서브시스템 단위로 큐를 썼던 것은 1개 이상의 액터로 대체된다.
재진입과 우선순위 지정
메인 액터: 메인스레드를 나타내는 액터
func loadArticle(with id: ID) async throws -> Article
@MainActor func updateUI(for article: Article) async
@MainActor func updateArticles(For ids: [ID]) async throws {
for id in ids {
let article = try await database.loadArticle(with: id) // context switch!
await updateUI(for: article) // context switch!
}
}
func loadArticle(with ids: [ID]) async throws -> Article
@MainActor func updateUI(for articles: [Article]) async
@MainActor func updateArticles(For ids: [ID]) async throws {
let articles = try await database.loadArticles(with: ids)
await updateUI(for: articles)
}