2025. 10. 4. 20:48ㆍiOS/WWDC 파보기
Swift Concurrency가 기존의 Threading Model에서 어떤 문제를 해결하기 위해 등장했는지 알아보겠습니다
Index
- Threading model
- Synchronization
Threading model


1. Main Thread에서 user event
2. Database에서 feed를 로드
3. 각 feed의 content를 concurrent Queue를 이용하여 네트웤크 fetch
4. result를 serial Queue를 이용하여 database에 적재
위의 흐름을 소스코드로 표현하면 다음과 같다.

위의 소스코드는 단점(pitfall)이 있는데 이를 이해하려면 기존의 GCD가 어떻게 동작하는 지 알아야한다.
GCD는 큐에 work item을 담고 thread로 가져와 작업을 수행한다.
concurrent queue는 수행할 작업의 수 만큼 작업을 thread에 담는다. 만약 interrupt이 발생하면 thread를 block하고 새로운 thread를 불러온다.
blocked thread가 리소스를 기다린다고 할 때, 즉, 세마포와 같은 작업을 기다린다면 새로운 thread를 불러와 작업을 수행한다.


위의 뉴스앱과 같이 concurrent queue와 database queue.sync 작업이 빈번하게 일어난다면, concurrent 작업이 block되고, sync 작업 switching이 일어나기 때문에 피드 수만큼 thread가 생성될 것이다.
GCD는 위와 같이 Thread 채로 block한다. 따라서 concurrent한 작업량이 많은 경우, 작업 수만큼 Thread가 늘어날 수 있다.
그렇다면 다음과 같이 물을 수 있다. Thread가 많아지는게 뭐 어때서?
CPU Core 보다 Thread 수가 많아질 수 있게 되는데, 이 현상을 Thread Explosion이라고 부른다. 이 때는 다음과 다음과 같은 사이드 이펙트가 발생한다.
- 데드락이 발생과 메모리 오버헤드
- 대기하는 동안 각 스레드들은 리소스를 가지고 있는데 각 중지된 스레드들은 스레드를 추적하기 위해 스텍과 그리고 관련있는 커널 데이터 구조를 가지고 있다. 만약 이 스레드들이 lock을 점유하고 있는 다면, 다른 스레드가 필요한 자원들에 대해서 접근을 막고 있는다.
- 스케줄링 오버헤드
스레드가 다시 스케줄링이 되면, context switching이 된다. 이 때 CPU로부터 time sharing이 되는데, 평시라면 문제가 없지만, thread explosion일 때는 엄청나게 비효율적으로 처리될 것이다.
Concurrency in Swift
Swift는 앞의 상황을 해결하기 위해 continuation을 도입하여 개선하였다.

Thread를 block하지 않고, 대신에 light-weight object인 Continuation을 도입하여 상태를 저장한다. 이를 통해 Thread context switching 비용을 줄였다. 즉, full context switching cost 대신 가벼운 function call cost를 들여서 같은 효과를 낸다.
Runtime에 위와 같은 작업을 수행하기 위한 contract가 필요한데, 다음과 같은 swift 언어적 기능이 추가되었다.(의역)
Language features
await and non-blocking of threads. 기다리되, thread를 blocking 하지는 않는다. Thread를 block하는 대신 task model에서 작업을 추적하기 위한 의존성을 다음과 같이 나타낼 수 있다.
Suspension Point - await
await 이전 이후를 기준으로 이후에도 필요한 정보는 async frame에, 아닌 경우는 stack frame에 저장한다.

sync func
sync func일 경우에는 stack frame을 쌓고, 완료되면 pop한다.
이미지에서 id와 article은 await 이전에 local var이나 parameter로 쓰이지 않았다. 즉, await 이전 이후에 접점이 없기 때문에 stack frame에 저장된다.
async func
- local variable을 stack frame에 저장한다. await을 만나면 heap에 frame을 저장한다. 이 때는 suspension point를 넘나들 필요한 정보들을 저장한다.
- 예를 들어, newArticles 파라미터는 await 이후에도 필요하기 때문에 asyncFrame에 저장한다.
- 제어권을 System에게 넘긴다.
- 시스템은 다른 useful한 작업을 시행한다. 작업이 종료되거나 await을 만나면 다시 제어권을 시스템에게 넘겨준다.
- continuation이 resume 되면, heap에서 context를 불러와 작업을 수행한다.
⚠️ continuation이 resume 될 때, 시작한 thread와 다른 thread일 수 있다.
비동기 작업 간의 Dependency
continuationn은 dependency가 있는데 선행 작업이 완료되어야 이후 작업이 실행 가능하다. 이 의존성은 Swift Concurrency Runtime에 의해 추적된다.
TaskGroup 또한 child task가 parent task에 대해 의존성을 갖는다.
child task가 완료되어야 parent task가 완료될 수 있다.
이는 Swift Compiler와 Swift Runtime에 의해 체크된다.
Cooperative thread pool
- Default Executor for Swift
- pool의 크기는 CPU Core 수
- Controlled granularity of concurrency
- Worker threads don't block
- Avoid thread explosion and excessive context switches
Thread를 점유하지 않고, continuation을 통한 상태를 저장하고, Thread는 반납한다. 반납된 Thread는 System에 의해 blocked되지 않고, 다른 작업을 수행한다.
이 작업이 수행되는 동안 Context Switching cost 대신 function cell cost가 된다.
Adoption of Swift Concurrency
Await and atomicity
- await을 기준으로 이전과 이후에 실행되는 thread가 같다는 보장을 할 수 없다.
- Can not hold locks across await
- Break atomicity by voluntarily descheduling the task
- Thread specific data is not preserved across an await / await 전후로 다시 체크해야한다.

다음과 같은 경우 보장되지 않는다.

이를 체크하기 위해 debug 환경 변수를 추가할 수 있다.
LIBDISPATCH_COOPERATIVE_POOL_STRICT=1
동기화 Synchronization
- Mutual exclusion
- Reentrancy and prioritization
- Main actor
Mutual exclusion
기존의 Serial Queue.sync { ... }
databaseQueue.sync {
updateDatabase(articles, for: feed)
}
- No contention
- 시리얼 큐에 단일 스레드만 접근하는 경우, 스레드 경쟁이 없을 때
- Reuse thread
- Work item on the queue는 context switch 없음
- 대신 항상 running 함
- Under contention
- 스레드에 경쟁이 어느 정도 있을 때
- thread blocking이 있을 때
| SerialQueue.sync | SerialQueue.async | Actors using cooperative pool thread | |
| No Contention | Reuse thread | ⚠️ Request new thread | Reuse thread |
| Under Contention | Blocking | Non-blocking | Non-blocking |
Contention은 여러 스레드가 동시에 동일한 자원(예: CPU, Memory, Lock) 등을 사용하려고 할 때 발생하는 경쟁 상태입니다.
No Contention: 경쟁이 전혀 없음
Under Contention: 약간의 경쟁이 있음(하지만 지나치지 않음)
High Contention: 많은 경쟁이 일어남 / 성능 저하 위험
Actor hopping

Database actor와 feed actor간의 switching이 발생하는 시나리오

Thread는 그대로 있고, actor context만 계속 Switching 된다. Thread를 점유하지 않음
DB Actor가 다른 thread가 필요하게 되면 해당 thread의 actor는 pending되고, DB Actor가 free하면 DB Actor가 작업을 이어서 수행하고, DB Actor가 다른 일을 수행하고 있다면 다른 pending Actor가 해당 thread를 점유해서 작업을 진행한다.
- Actor hopping이 발생할 때, thread가 block 되지 않음
- hopping은 다른 thread가 필요하지 않는다. 동일한 thread에서 가능
Priority와 Reentrancy

큐가 위와 같을 때, 우선 순위가 높은 B 작업이 추가된다고 해보자
A가 실행되고 난 후, B의 우선순위가 높지만, 백그라운드 작업이 앞에 존재한다.
이를 우선 순위 역전이라고 한다.
우선순위가 높은 B를 실행시키기 위하여 앞에 있는 background queue work item의 우선 순위를 강제로 높여서 실행시킨다. 하지만 근본적인 해결책은 아니다.
💫 위와 같은 문제를 해결하기 위해 actor가 등장했다.

actor는 FIFO를 따르지 않고, 우선 순위가 높은 작업을 앞으로 끌어온다. 즉, Actor에 pending된 작업이 있다고 해도, 신규 작업의 우선순위가 높다면 pending돈 것보다 먼저 처리할 수 있다. 또한 이 일은 runtime에 일어난다.
예시 시나리오 - 네트워킹과 UI 업데이트


Actor hopping이 일어나고 있다.
만약 더 많은 아티클을 불러온다고 하면 어떻게 될까?

스위칭이 발생이 빈번하게 되고 성능 저하가 발생한다. 이를 해결하기 위해서는 소스 코드 구조를 바꿀 필요가 있다. 각각의 값에 UI를 업데이트 하지 않고, 로드를 한번에 하고 이후에 UI를 업데이트하게 변경

'iOS > WWDC 파보기' 카테고리의 다른 글
| [HIG] UX Writing - 인터페이스를 위한 글쓰기 WWDC 2022 (0) | 2025.11.17 |
|---|---|
| [HIG] UX Writing - 작은 텍스트 변화로 UI에 큰 임팩트 만들기 (0) | 2025.11.14 |
| WWDC2019 - Advances in Networking, Part 1 (0) | 2024.01.24 |
| Apple Design Challenge Part 1 - 컨설팅 후기 (0) | 2023.03.15 |
| Apple Design Challenge Part 1 (5) | 2023.03.15 |