우리가 작성한 await 코드는 실제로 어떻게 쪼개지고 힙에 저장될까? 로우레벨 SIL로 분석한 Swift Concurrency의 민낯
애플 플랫폼 동시성 모델의 진화 과정을 NSThread부터 GCD Block, 그리고 Swift Concurrency의 Task까지 흐름에 따라 입체적으로 다룹니다. 채널톡 기술 팀이 Swift 6 도입 전 과도기 코드가 실제로 어떻게 안전하게 동작하는지를 바이너리 심볼과 SIL 명령어 레벨까지 파고들어 명쾌히 풀어낸 탐구서입니다.
Swift 6 도입을 앞두고 있거나 기존 RxSwift 코드베이스와의 동시성 간극을 해결하고자 하는 모바일 아키텍트 및 실무 iOS 개발자에게 깊이 있는 런타임 통찰을 제공합니다.
애플 플랫폼에서 RxSwift와 Swift Concurrency가 혼용될 때 컴파일러가 둘 사이의 안전한 상호운용성을 완전히 보장해주지 못하며, GCD 기반의 블록 모델은 대기 상태 시 스레드 폭발을 유발하는 구조적 한계를 가지고 있었습니다.
Swift Concurrency는 컴파일러가 await 지점에서 함수를 조각내어 부분 함수로 분할하고, 살아남아야 할 상태값을 힙의 태스크 컨텍스트에 할당하여 스레드를 점유하지 않고 반납할 수 있는 태스크 모델을 도입했습니다.
바이너리 역어셈블 및 SIL 분석을 통해 RxSwift와 Swift Concurrency가 공존하는 코드에서 스레드 폭발을 방지하고 고정된 스레드 풀 위에서 안전하게 컨텍스트가 전환되는 동작 원리를 하부 레벨에서 증명했습니다.
Trade-off
Swift Concurrency의 고정 크기 스레드 풀 내에서 동기식 대기(blocking)가 발생하면 풀 전체가 마비될 수 있으며, 컴파일러가 이러한 동기 대기 코드를 감지하지 못하므로 forward progress 보장 규칙을 개발자가 엄격히 준수해야 하는 한계가 존재합니다.
GCD 큐에서 실행 중인 블록들이 동기식 대기나 세마포어 등에 걸렸을 때, 시스템이 코어를 놀리지 않으려고 임시방편으로 스레드를 지속해서 새로 생성하면서 컨텍스트 스위칭 비용과 메모리 점유가 기하급수적으로 증가하는 현상입니다.
Swift Concurrency에서 await 키워드를 기점으로 컴파일러가 원본 비동기 함수를 실제로 잘라내어 바이너리에 생성하는 여러 조각의 개별 서브 함수들을 의미합니다.
일시 중단(Suspension)되었던 태스크가 다시 실행(Resume)될 때 자신이 안전하게 속해야 하는 실행 컨텍스트(Executor/Actor)로 스레드를 강제로 전환 및 매핑해 주는 SIL 레벨의 핵심 명령어입니다.