- Meet async/await in Swift
- Discover concurrency in SwiftUI
- Swift concurrency: Behind the scenes
Meet async/await in Swift
소개
비동기 코드는 장황하고, 복잡하며 부정확하게 작성하기 쉬우며, 이러한 코드에 async/await이 도움이 될 수 있다.
async/await을 사용하면 일반 코드를 작성하는 것처럼 쉽게 비동기 코드를 작성할 수 있다.
기존 SDK에 사용할 수 있는 수많은 비동기 코드들이 있다.
동기 vs 비동기 코드
동기 함수를 호출하면 스레드가 차단되어 해당 함수가 완료되기를 기다리고 있고, 따라서 스레드는 다른 아무런 작업도 할 수 없다.
만약, 컴플리션 핸들러를 사용하는 비동기 함수를 호출하면 스레드는 해당 시간 동안 다른 작업을 할 수 있게 된다.
그리고, 작업이 완료되면 컴플리션 핸들러를 통해 작업의 완료를 알린다.
비동기 함수의 공통점은 함수를 호출하면 스레드의 차단을 해제하여 다른 작업을 시작할 수 있게 해 준다.
이 말은 오랜 시간이 걸리는 작업이 수행되는 동안 스레드가 다른 일을 할 수 있게 해 준다는 의미이다.
비동기 코드 예
func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
let request = thumbnailURLRequest(for: id)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(nil, error)
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
completion(nil, FetchError.badID)
} else {
guard let image = UIImage(data: data!) else {
completion(nil, FetchError.badImage)
return
}
image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
guard let thumbnail = thumbnail else {
completion(nil, FetchError.badImage)
return
}
completion(thumbnail, nil)
}
}
}
task.resume()
}
위의 코드는 서버에 저장된 이미지의 썸네일을 표시하는 함수이다.
일부 작업은 값을 빠르게 반환하며 이러한 함수가 동기식으로 호출되는 것은 괜찮다.
하지만, 이미지를 다운로드하거나, 썸네일을 렌더링 하는 값 비싼 작업은 수행 시간이 오래 걸리고, 이는 비동기적으로 수행되어야 한다.
컴플리션 핸들러 문제점
값을 반환할 준비를 성공적으로 마치거나, 오류가 발생할 시 컴플리션 핸들러를 통해 이를 전달할 수 있다.
하지만, 컴플리션 핸들러는 모든 경로의 반환을 보장하지 않아서 실수로 컴플리션 핸들러를 호출하는 것을 잊는 등의 문제가 발생해도 Swift는 이를 알리지 못한다.
이에 Result 타입을 사용할 수 있다. 이 방법은 좀 더 안전하지만 절차를 추가하여 코드의 가독성을 낮춘다.
async/await
async/await을 사용하면 비동기 코드를 더 간단하게 코드를 작성할 수 있으며, 컴플리션 핸들러와 달리 Swift가 컴파일 타임에 값 혹은 오류에 대한 반환을 검사한다.
async/await은 함수뿐 아니라 getter, for, sequence 등 다양한 곳에 사용할 수 있다.
Suspend
await 키워드는 비동기 함수가 중단(suspend)될 수 있음을 나타낸다.
비동기 함수가 중단된다는 것이 무엇을 의미하는지 알아보기 위해 일반적인 함수의 수행 과정을 살펴보자.
일반적인 함수를 호출할 때, 함수는 실행 중인 스레드의 제어권을 가진다.
이는 함수의 작업이 수행하는 동안 유지되고, 값 혹은 오류를 반환하면 함수는 다시 스레드의 제어권을 시스템에 넘겨준다.
만약, 호출하는 함수가 비동기 함수라면 수행 과정은 달라진다.
일반적인 함수와 마찬가지로 비동기 함수를 호출할 때, 스레드에 대한 제어권을 부여한다. 그러다 중단점을 만나면 함수는 스레드에 대한 제어권을 포기한다.
이때 스레드의 제어권을 시스템에 제공하게 되고, 현재 함수는 일시 중지된다.
시스템은 현재 남아있는 작업에 따라 현재 스레드에서 다른 작업을 수행하거나, 기존 함수를 이어서 수행할 수 있고 혹은 다른 스레드에서 기존 함수를 이어서 수행한다.
이는 await을 만나 함수가 일시 중지 된다고 하더라도 무조건 정지하는 것을 의미하지 않으며 또한 기존 함수가 기존 스레드에서 수행하는 것만을 의미하지 않는다.
정리하자면, 비동기 함수가 수행될 때 await 키워드를 만나면 스레드의 제어권을 시스템에 넘기고, 시스템은 현재 남아있는 작업들에 따라 급한 작업을 먼저 수행하거나, 기존 스레드 혹은 다른 스레드에서 작업을 이어나간다.
Discover concurrency in SwiftUI
소개
Swift 5.5는 Swift 코드에서 동시성을 관리하기 위한 새로운 도구를 도입
- 이 영상에서는 이러한 개선 사항이 SwiftUI 앱과 어떻게 상호 작용하는지 이해를 도우며, 새로운 도구가 데이터 모델을 개선하는 방법과 Main Actor의 동작에 대해 설명
SwiftUI Update Life Cycle
우선 SwiftUI가 ObservableObject와 어떻게 상호 작용하는지에 대해서
라이프 사이클을 구동하는 코드를 런 루프라고 한다.
런 루프는 메인 액터에서 실행되며, 사용자로부터 이벤트를 수신하고, 모델을 업데이트한 다음 SwiftUI 뷰를 화면에 렌더링 한다.
- 이 업데이트를 런 루프의 틱이라고 부르고 있다.
SwiftUI에서 ObservableObject는 런 루프와 몇 가지 흥미로운 방식으로 상호 작용할 수 있다.

updateItems 함수를 살펴보자
- SwiftUI 뷰에서 updateItems를 호출할 것이고, 메인 액터에서 실행될 것이다.
- 파란색 사각형은 updateItems가 실행되는 시간이다.
- fetch 된 사진을 items 속성에 할당하는 코드에 집중해보면 items은 published된 속성이기 때문에, 이 할당은 objectWillChange 이벤트를 트리거하고 직후에 fetch된 사진을 items의 저장소에 기록한다.
SwiftUI가 이 objectWillChange를 볼 때, items의 스냅샷을 찍는다.
- 스냅샷 후 런 루프의 다음 틱에서 SwiftUI는 스냅샷을 현재 값과 비교하고, 값이 다르기 때문에 SwiftUI는 Photos에 의존하는 뷰를 업데이트해야 하는 것을 알고 있다.
- objectWillChange, 저장소 업데이트, 런 루프 틱은 모두 메인 액터에서 발생하기 때문에 순서대로 발생하는 것이 보장된다.
Blocking run loop

메인 액터에서 많은 작업을 하는 경우 느린 업데이트가 발생할 수 있다.
- 예를 들어, 느린 네트워크로 인해 fetchPhotos 함수에서 blocking이 발생했다고 가정하면, 메인 액터가 blocking 되고 있기 때문에 런 루프의 틱을 놓친다.
- 이 상황은 사용자에게 히치(hitch)처럼 보인다.

과거에는 이러한 작업을 다른 큐(DispatchQueue)에서 수행하도록 할 수 있었으며, 이는 잘 작동하는 것처럼 보인다.
- 하지만, 여기에는 까다로운 문제가 있다.
- 변경사항과 런 루프 틱이 인터리브 할 수 있다.
- 예를 들어, items에 할당하고, SwiftUI가 objectWillChange 스냅샷을 찍었을 때, 런 루프 틱의 바로 전에 발생할 수 있다.
- 아직 상태 변경이 발생하지 않은 상태이므로, SwiftUI는 스냅샷을 변경되지 않은 값(이전 값)과 비교한다.
- 실제 상태 변경은 런 루프 틱 후에 발생하지만, SwiftUI는 그 변경을 감지할 수 없기 때문에 뷰가 업데이트되지 않는 상황이 발생할 수 있다.
- 이러한 문제를 방지하기 위해서 SwiftUI에서 이벤트는 순서대로 발생하여야 한다.
-
- objectWillChange
-
- ObservableObject의 상태 업데이트
- 런 루프의 다음 틱 도달
-
- 이 모든 과정이 메인 액터에서 발생한다는 것을 확신할 수 있다면, 순서를 보장할 수 있다.
- 혹은 상태를 업데이트하기 전에 메인 큐로 다시 보냈을 것이다.
async/await
이젠 이러한 과정이 훨씬 쉬워졌다.
- await 키워드를 사용함으로써 메인 액터로부터 비동기 호출을 한다.
- 비동기 작업을 진행하는 동안 메인 액터에게 다른 작업을 계속하도록 한다.
- yielding이라고 불린다.
/// An observable object representing a random list of space photos.
@MainActor
class Photos: ObservableObject {
@Published private(set) var items: [SpacePhoto] = []
/// Updates `items` to a new, random list of `SpacePhoto`.
func updateItems() async {
let fetched = await fetchPhotos()
items = fetched
}
/// Fetches a new, random list of `SpacePhoto`.
func fetchPhotos() async -> [SpacePhoto] {
var downloaded: [SpacePhoto] = []
for date in randomPhotoDates() {
let url = SpacePhoto.requestFor(date: date)
if let photo = await fetchPhoto(from: url) {
downloaded.append(photo)
}
}
return downloaded
}
/// Fetches a `SpacePhoto` from the given `URL`.
func fetchPhoto(from url: URL) async -> SpacePhoto? {
do {
let (data, _) = try await URLSession.shared.data(from: url)
return try SpacePhoto(data: data)
} catch {
return nil
}
}
}


updateItems 함수에서 오래 걸리는 작업을 수행하는 동안 메인 액터를 SwiftUI에 반환하기 위해 await을 사용하므로, 런 루프를 계속 돌아가게 만들고, UI 히치를 피할 수 있다.
- 비동기 작업이 완료되면
- updateItems 함수는 다시 메인 액터에 진입
- items 속성에 값을 안전하게 업데이트
- objectWillChange 트리거
- SwiftUI에서 새로운 값을 사용
main actor
작업이 메인 액터에서 실행되는 한, objectWillChange 버그가 발생하지 않는다고 확신할 수 있다.
이를 Swift 컴파일러가 보장한다.
@MainActor 어노테이션을 추가하면, 컴파일러는 속성과 함수가 메인 액터에서만 액세스 할 수 있도록 보장한다.
Swift concurrency: Behind the scenes
소개
Swift 동시성에 대한 고급 영상으로 language-safe, 성능, 효율 그리고 설계에 대해서 이야기
- Swift 동시성 뒤에 있는 스레딩 모델에 대한 이야기와 GCD와의 비교
- 동시성 언어 함수를 활용하여 Swift를 위한 새로운 스레드 풀 구축으로 성능과 효율성을 어떻게 개선했는지
- Swift 동시성을 사용하도록 코드를 포팅할 때 염두해야 할 고려사항
- Swift 동시성 동기화(synchronization)
- actor 작동 방식과 GCD와 같은 기존 타입과 비교
- actor를 사용한 코드를 작성할 때 염두해야할 사항
스레딩 모델
뉴스 피드 리더 앱을 만들고 싶을 때, 이 앱의 높은 수준의 구성 요소는 아래와 같다.
- 사용자 인터페이스를 구동할 메인 스레드
- 사용자가 구독한 뉴스 피드를 추적하는 데이터베이스
- 피드의 최신 콘텐츠를 가져오기 위한 네트워킹 로직 처리 하위 시스템
GCD
func deserializeArticles(from data: Data) throws -> [Article] { /* ... */ }
func updateDatabase(with articles: [Article], for feed: Feed) { /* ... */ }
let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: concurrentQueue)
for feed in feedsToUpdate {
let dataTask = urlSession.dataTask(with: feed.url) { data, response, error in
// ...
guard let data = data else { return }
do {
let articles = try deserializeArticles(from: data)
databaseQueue.sync {
updateDatabase(with: articles, for: feed)
}
} catch { /* ... */ }
}
dataTask.resume()
}
위 코드는 각 피드의 최신 값으로 데이터베이스를 동기적으로 업데이트하고, 캐싱한다.
- 하지만, 몇 가지 숨겨진 성능 함정이 있다.

성능 문제를 이해하려면 GCD 큐에서 작업을 처리하기 위해 스레드가 어떻게 생성되는지 알아야 한다.
- GCD에서 작업이 큐에 추가되면, 시스템은 해당 작업을 처리하기 위한 스레드를 불러온다.
- 동시 큐는 한 번에 여러 작업 항목을 처리할 수 있기 때문에, 시스템은 모든 CPU 코어를 포화시킬 때까지 여러 스레드를 불러온다.
- 이때, 스레드가 blocking 되고, 동시 큐에서 더 많은 작업을 수행해야 한다면 GCD는 나머지 작업을 위해 더 많은 스레드를 불러온다.
- 이유:
- 프로세스에 또 다른 스레드를 제공함으로써, 각 코어가 작업을 실행하는 스레드를 계속 제어하도록 보장할 수 있음 -> 지속적인 수준의 동시성 제공
- blocking 된 스레드는 작업을 계속하기 위해 세마포어와 같은 자원을 기다리고 있을 수 있음 -> 큐에서 작업을 계속하기 위해 새로운 스레드가 첫 번째 스레드의 대기 중인 리소스의 차단을 해제할 수 있음
- 이유:

돌아와서 뉴스 앱 코드의 CPU를 살펴보자.
- 애플 워치와 같은 듀얼 코어 장치에서 GCD는 피드 업데이트 결과 처리를 위해 두 개의 스레드를 불러옴
- 스레드가 데이터베이스 큐에 액세스 하는 것을 차단함에 따라, 네트워킹 큐에서 작업을 계속하기 위해 더 많은 스레드 생성
- CPU는 다양한 스레드 사이에서 네트워킹 결과를 처리하는 서로 다른 스레드 간의 context change를 해야 함
- 이는 많은 수의 스레드가 불러와질 수 있음을 의미
만약, 업데이트해야 할 피드가 100개인 경우 GCD는 데이터베이스 큐에서 각 콜백 블록마다 스레드를 불러올 것이고, 결국 매우 많은 수의 스레드가 불러와진다.
스레드
많은 수의 스레드가 존재하면 안 좋은 점은 시스템이 CPU 코어보다 많은 수의 스레드로 과도하게 커밋된다는 의미
- 6개의 CPU 코어에 100개의 피드 업데이트가 있다면, 코어보다 16배 더 많은 스레드로 과도하게 커밋한 것
- 이를 thread explosion이라고 부르며, thread explosion은 메모리와 스케줄링에 오버헤드를 발생시킨다.
메모리 오버헤드

- block 된 각 스레드는 다시 실행되기를 기다리는 동안, 메모리와 자원을 보유하고 있는다.
- 그리고 스레드를 추적하기 위한 스택, 관련 커널 데이터 구조가 있음
- 이러한 스레드 중 일부는 다른 스레드에 필요한 lock을 점유하고 있는다.
- 이것은 작업이 진행되지 않는 스레드가 필요한 많은 수의 자원 혹은 메모리
스케줄링 오버헤드
새 스레드가 생성되면, CPU는 이전 스레드에서 새 스레드 실행을 위해 전체 스레드 context switching을 수행해야 한다.
block된 스레드가 다시 실행 가능해짐에 따라, 스케줄러는 CPU의 스레드를 시분할하여 작업을 수행할 수 있도록 함

하지만, thread explosion이 발생하면, 제한된 코어로 수백 개의 스레드를 시분할해야 하는 상황이 발생하며, 과도한 context switching으로 이어진다.
따라서, CPU의 효율이 떨어지게 된다.
소 결론
이러한 문제점들로 인해 GCD 큐로 코드를 작성할 때, thread sanitizer에 대한 이러한 부분을 놓치기 쉽고, 성능이 저하되고 오버헤드가 커지는 문제가 생긴다.
Concurrency
Swift는 언어에 동시성을 설계할 때, 다른 접근 방식을 취했다.
- 통제되고, 구조화되고, 안전하게 동시성을 사용할 수 있도록 성능과 효율성을 염두
Continuation

Swift 동시성을 사용하면, 스레드 대신 Continuation이라는 경량 객체를 사용한다.
- 작업을 진행할 때, 스레드 context switching을 하지 않음
- 대신, continuation을 전환함
- 결과적으로 비동기 작업을 위해 함수 호출 비용만 지불
Swift 동시성의 목표:
- 런타임 동작동안 CPU 코어의 수만큼의 스레드를 만드는 것.
- 스레드가 block 되었을 때, 작업 사이를 저렴하고, 효율적으로 전환하는 것
비동기 함수
스레드의 동작 방식
실행 중인 프로그램의 모든 스레드에는 하나의 스택이 있다.
- 함수 호출을 위한 상태를 저장
하나의 스레드의 동작을 살펴보면
- 스레드가 함수 호출을 실행하면 새로운 프레임이 스택에 푸시된다.
- 새로 생성된 스택 프레임은 함수에서 필요한 로컬 변수, 반환 주소, 기타 필요한 정보를 저장하는 데 사용
- 함수의 실행을 마치면 스택 프레임이 팝 된다.
비동기 함수의 동작 방식
// on Database
func save(_ newArticles: [Article], for feed: Feed) async throws -> [ID] { /* ... */ }
// on Feed
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 updateDatabase(with articles: [Article], for feed: Feed) async throws {
// skip old articles ...
try await feed.add(articles)
}

updateDatabase 함수 호출

feed.add() 호출
- 스레드의 스택 프레임에 add()가 push 됨

스택 프레임은 suspension point(await)에서 사용할 필요가 없는 로컬 변수를 저장
- 로컬 변수인 id, article은 정의된 후 for 루프에서 즉시 사용되며, 그 사이에 suspension point가 없다.
- id, article은 스택 프레임에 저장됨

힙에는 두 개의 비동기 프레임이 저장된다.
- updateDatabase()
- add()
비동기 프레임은 suspension point에서 사용할 수 있는 정보를 저장
- newArticles -> add()
- newArticles는 await 전에 정의되지만 await 후에 사용할 수 있음을 유의
- 이는 add()를 위한 비동기 프레임이 newArticles를 추적한다는 의미

save()가 실행되기 시작하면, add()를 위한 스택 프레임이 save()를 위한 스택 프레임으로 대체됨
- 새로운 스택 프레임을 추가하지 않음
- 미래에 필요한 변수가 이미 비동기 프레임에 저장되어 있기 때문
이후 save()를 위해 비동기 프레임이 추가됨

save()가 실행되는 동안, 스레드는 차단되지 않음.
- 대신 다른 작업을 수행하기 위해 재사용되며, suspension point에 필요한 정보는 힙의 비동기 프레임에 저장되어 있어 나중에 실행을 이어서 할 수 있다.
- 비동기 프레임은 continuation의 런타임 표현



save() 요청이 완료되면
- save 스택 프레임을 다시 add 스택 프레임으로 대체
- 이후 스레드는 zip()을 수행
- 새로운 스택 프레임 생성
- zip()이 완료되면 다시 팝
- 이전 과정 반복, ...
Cooperative thread pool
Swift 동시성 코드로 스레드가 작업을 계속 진행할 수 있는 런타임 계약을 유지할 수 있게 됨
- 이 런타임 계약을 활용하여 통합 OS 지원을 구축했다.
- 이게 협력 스레드 풀의 형태
협력 스레드 풀은
- default executor로서 Swift 동시성을 뒷받침
- CPU 코어의 수만큼만 스레드를 생성
- 시스템의 overcommit 방지
- GCD의 concurrent queue와 달리 worker 스레드를 block 하지 않음
- 즉, thread explosion과 과도한 context switch가 방지된다.
actor
actor를 사용하면 mutatable 한 상태를 동시 접속으로부터 보호할 수 있다.
- actor는 mutual exclusion을 보장
- 한 번에 최대 하나의 메서드를 호출
- 데이터 레이스를 방지
- 효율적인 스케줄링을 위해 협동 스레드 풀 활용
- 아무런 작업도 실행되지 않는 액터에서 메소드를 호출할 때, 호출 스레드는 메소드를 실행하기 위해 재사용될 수 있음
- 액터가 어떤 작업을 실행 중인 경우, 호출 스레드는 실행 중인 함수를 일시 중지하고, 다른 작업을 선택할 수 있음
협동 스레드 풀 활용 예

액터들은 협동 스레드 풀에서 실행된다.
- 예를 들어, feed 액터는 database 액터와 상호 작용하여 기사를 저장하거나 다른 작업 수행
- 이 상호 작용은 어떤 actor에서 다른 actor로 excution switching을 포함
- 이 과정을 actor hopping이라고 부름
actor hopping 과정

스포츠 피드 액터가 협동 스레드 풀에서 실행 중이고, 일부 article을 데이터베이스에 저장
- 현재 데이터베이스 액터는 사용 중이지 않다.

스레드는 스포츠 피드 액터에서 데이터베이스 액터로 직접 이동할 수 있다.
- 여기서 주목해야 할 점은 액터를 호핑 하는 동안 스레드가 block 되지 않음, 호핑은 다른 스레드를 사용하지 않는다.
- 런타임은 스포츠 피드 액터에 대한 작업을 일시 중지하고, 데이터베이스 액터에 대한 새로운 작업을 생성

데이터베이스 액터가 작업을 수행하다가, 날씨 피드 액터가 데이터베이스에 article을 저장하려고 한다면?
- 데이터베이스 액터는 새로운 작업을 생성
- 액터는 상호 배제를 보장함으로써 안전을 보장
- 현재 활성화 중인 작업(D1)이 존재하기 때문에 새로운 작업(D2)은 보류 상태로 유지

날씨 피드 액터는 정지되고, 다른 작업을 할 수 있게 됨
- 건강 피드 액터의 작업을 수행

기존 D1 작업 완료
- 이 시점에서 런타임은 보류 중인 작업 중 하나를 선택하여 작업 재게
- D2 혹은 W1
재진입, 우선순위
많은 비동기 작업이 있을 때, 시스템은 어떤 작업이 중요한지에 따라 수행 순서를 정해야 한다.
- 이상적으로, 사용자 상호 작용과 관련된 작업의 우선순위가 높음
- 백업과 같은 백그라운드 작업의 우선순위는 낮음
액터는 재진입의 개념으로 시스템이 작업의 우선순위를 결정할 수 있도록 설계됨
GCD의 우선 순위

GCD의 우선 순위 처리 과정
- 디스패치 큐는 작업을 FIFO로 실행한다.
- A -> 1 -> 2 -> 3 -> 4 -> 5 -> B -> 6 -> 7
- 사용자 상호 작용 작업(B)을 수행하기 위해선 중요하지 않은 작업인 백그라운드 작업(1, 2, 3, 4, 5)을 먼저 수행해야 함
- 이를 우선순위 역전이라고 함

디스패치 큐는 우선순위가 높은 작업보다 앞서 있는 작업의 우선 순위를 높임
- 이 방법으로 우선 순위 역전의 문제를 해결할 수 있다.
- 하지만, 작업 B를 수행하기 위해 작업 1 ~ 5를 수행해야 한다는 문제는 해결하지 못함.
액터의 재진입



스레드에서 데이터베이스 액터가 작업 중 일시 정지되었음을 가정할 때
- D1의 작업을 대기하는 동안 스포츠 피드 액터가 이 스레드에서 작업(S1)을 수행한다.
- S1 작업이 데이터베이스 액터를 통해 데이터를 저장
- 스레드는 정지된 작업 D1이 있음에도, D2 작업을 수행할 수 있음
- 이것이 액터 재진입



액터 재진입은 액터가 엄격한 FIFO가 아닌 우선순위로 작업을 수행할 수 있다는 것을 의미
- 중요한 작업 B의 앞에 중요하지 않은 작업들이 있지만, 런타임은 우선순위가 높은 작업을 큐의 맨 앞으로 이동하도록 선택할 수 있다.
- 우선 순위가 높은 작업이 먼저 실행되고, 낮은 작업이 나중에 수행됨
- 우선 순위 역전 문제를 직접 해결
- 스케줄링과 자원 활용도 상승
메인 액터의 액터 호핑

메인 액터는 메인 스레드를 추상화한다.
- 메인 스레드는 협동 스레드 풀과 분리되어 있음
- context switching이 발생

따라서 다음 코드는 스레드에 오버헤드가 발생
- loadArticle은 데이터베이스 액터에서 수행하는 반면, updateUI는 메인 액터에서 수행하기 때문
- 또한, for 문으로 인해 context switching이 빈번하게 발생

이러한 형태의 코드는 과도한 context switching이 발생할 수 있다.
- 대신, 메인 액터에 대한 작업이 일괄 처리되도록 코드를 재구성하면 됨
- loadArticle -> loadActicles
- updateUI(article) -> updateUI(articles)
- 작업을 일괄적으로 처리하면 context switching의 수가 줄어듦
- 협동 스레드 풀에서 액터 사이를 호핑 하는 작업은 빠르지만, 메인 액터와의 호핑을 염두해 코드를 작성해야 한다.
'Swift' 카테고리의 다른 글
Swift: if let, guard let 속도 비교 (0) | 2025.03.30 |
---|---|
Swift: async let 수행 순서 (0) | 2025.03.30 |
알쓸스잡 - 8 (0) | 2025.02.02 |
알쓸스잡 - 7 (0) | 2025.02.01 |
알쓸스잡 - 6 (0) | 2025.01.21 |