3일차 주제: continuation
안녕하새요..
벌써 3회차라니
Continuation 들어가기
저번 시간에 async/await을 살펴보면서, 잠시 continuation을 언급했어요.
저는.. 음..
다시 되짚어 볼까요??
continuation을 언급하면서,
위처럼 완료 핸들러 기반의 기존 함수를 async/await 기반으로 바꾸기 위해
async 함수로 기존의 함수를 감싸는 방식을 활용하는 예시를 들었는데요.
하지만 완료 핸들러를 사용하고 있는 기존 함수는 await을 사용하는 것처럼,
데이터가 반환하는 것을 기다릴 수가 없었습니다.
이에, 위처럼 내부적으로 Core Data를 호출해서,
Core Data에게 대신 비동기 작업을 기다리고 결과를 반환하는 bridge 역할을 하도록 구현할 수 있었습니다.
이러한 패턴을 continuation이라고 불렀어요.
Continuation
- 비동기 코드 내에서 일시 중지와 재개 지점을 관리하는 객체
- 동기 코드를 비동기 코드로 연결하는 메커니즘
이는 중단 지점을 관리하는 객체를 생성해서, resume 될 때까지 일시 중지된 상태로 유지하게 하는데요.
중단했다가 다시 재개하는 것이 가능한 이유는, 중단 지점의 상태값들을 heap에 저장하기 때문입니다.
예를 들어서, 위와 같이 비동기 함수 add()가 있다고 가정해볼게요.
해당 함수에는 await (중단점) 이전과 이후 모두에 사용되는 newArticles라는 정보가 있는데,
이러한 값들이 heap 메모리에 저장되게 됩니다.
여기에서 모든 변수들이 중단점과 관련이 있는 건 아니에요.
위에서 id, article 변수는 선언하자마자 바로 for 루프 안에서 사용되고, 그 사이에는 await가 없는데요.
이는 중단 없이 계속 사용되기에, 따로 보관해 둘 필요가 없어, 해당 변수들은 스택 프레임에 저장됩니다.
(일반 함수와 동일)
이렇게 중단 지점에서의 필요한 정보들을 heap에 저장하고,
이 정보들을 활용하면 다시 재개되어도 예전 상태 그대로 실행될 수 있는데요.
이러한 async 프레임의 목록들이 Swift 런타임에서 말하는 continuation 이라 합니다.
continuation이 저장되어 있기 때문에, 어떤 스레드가 작업을 이어서 해도 상관 없게 됩니다.
Continuation의 생성
Continuation이 생성하는 메서드에는 두 가지 종류가 있습니다.
- 런타임에서 체크 (안전): Checked Continuation
- 런타임에서 체크하지 않음 (안전 X): UnsaveContinuation
각각의 메서드들은 또 에러 처리를 할 수 있는 것과, 없는 것으로 나뉩니다.
여기에서 Checked는 continuation이 한 번도 호출되지 않거나,
여러번 호출되는 것을 런타임에서 자동으로 감지하기 때문에 상대적으로 안전하나
Unsafe는 이를 런타임에 확인하지 않으므로 안전하지 않습니다.
아래 예시에서는 Checked 종류의 함수인 withCheckedThrowingContinuation를 사용하고 있는데요.
이 때 resume 메서드로 중단 되었던 작업을 다시 실행하는데, 반드시 한 번 resume 메서드를 호출해야 합니다.
- 그 이유는 작업이 완료되었는데 결과를 한 번 더 보내면 데이터가 손상될 위험이 있기 때문입니다.
반대로 resume이 호출되지 않으면, 비동기 호출이 다시 재개되지 않아 메모리 누수 문제로 이어집니다.
async/await 방식으로 자동 전환
아래와 같이, Xcode의 Refactor를 활용해, 반자동으로 async/await 적용이 가능합니다.
- Add Async Alternative → 비동기 방식으로 완전히 전환
- Add Async Wrapper→ 기존의 콜백 방식의 함수를 그대로 놓아두고, Continuation 활용
Behind the scenes: GCD와 비교해서
아하하 저는 오늘 Swift concurrency: Behind the scenes를 보고 왔는디요.
위에서 언급한 Continuation과 더불어,
Swift concurrency와 GCD와의 성능적 차이점을 설명해보려 합니다.
예를 들어, 위처럼
- 사용자 인터페이스를 구동하는 메인 스레드
- 사용자가 구독한 뉴스 피드를 추적하는 데이터베이스
- 네트워킹 로직을 처리하는 서브시스템
의 구조를 가진 앱이 있다고 가정해보았습니다.
이전 GCD에서의 방식을 생각해보면,
- 네트워크 요청을 nn개 보내고 응답을 받는 동작: 동시 큐
- 여러 작업을 동시에 처리하기 위해
- 데이터베이스에 데이터를 업데이트하는 동작: 직렬 큐
- 데이터가 꼬이지 않기 위해서, 순차적으로 진행
위와 같이 각각의 상황에 맞는 큐를 사용하는데요.
하지만 여기에서 큰 성능 문제가 있습니다.
예를 들어서, 위처럼 스레드가 데이터베이스에 접근하려고 하다가,
어떤 작업이 먼저 데이터베이스를 수정하고 있으면 앞선 작업을 기다리잖아요?
이 때 GCD는 새로운 스레드를 생성해버립니다. !!!
스레드를 새로 만드는 이유는,
- 모든 CPU 코어가 계속 일하게 하기 위해서입니다.
- 코어를 최대한 효율적으로 운용하기 위해, 다른 스레드를 만들어 남은 작업을 코어에게 맡겨요.
- 기다리는 (블록된) 스레드를 도와주기 위해서입니다.
- 멈춘 스레드는 보통 어떤 자원(세마포어, 락 등)을 기다리고 있습니다.
- 이때 새로 만들어진 스레드가 해당 자원을 먼저 풀어줄 수 있기에, 멈췄던 스레드도 다시 실행할 수 있어요.
아무튼… 이렇게 새 스레드를 만들어 해당 스레드를 실행하는 과정은, 컨텍스트 스위칭이 일어난다는 것인데요.
이 때 컨택스트 스위칭을 위해서 CPU의 추가적인 비용이 들어가기 때문에, 비효율적입니다.
또한 스레드 하나를 만들게 되면,
- 각각의 스레드는 자신만의 메모리 공간(스택)을 가지고 있음
- 운영체제 커널도 각 스레드를 관리하기 위한 데이터 구조를 유지
- 일부 스레드는 lock을 쥐고 멈춰있는 상황
으로 시스템 자원을 계속 차지하고 있는데요.
그러니 GCD가 스레드를 nn개 만든다면 (스레드 폭발 현상),
- 메모리 및 리소스 문제
- CPU가 컨택스트 스위칭을 계속해야 함
의 문제가 있는 것입니다.
지금까지 살펴본 것처럼, GCD 큐만 사용해서 앱을 만들다보면 스레드가 너무 많아지고,
성능 저하 및 리소스 낭비 문제가 발생하기 쉽습니다.
이러한 경험들을 바탕으로, Swift 동시성 설계가 이루어졌는데요.
위에서 언급한 Continuation 개념이 등장합니다!
차이점이 보이시나요?
이전에는 중단할 때마다 스레드가 생성되었는데, 이제는 Continuation이라는 가벼운 객체로 처리합니다.
바뀐 모델에서는 CPU 코어 수만큼만 스레드를 만들고, 해당 스레드들을 멈추지 않는다고 약속합니다.
이를 "해당 앱의 스레드가 멈추지 않는다는 런타임 계약"이라고 언급합니다.
스레드를 생성하는 것보다, Continuation이 필요한 정보만 갖고 있다가 이어서 실행하기 때문에 훨씬 저렴합니다.
또한, 컨텍스트 스위칭 대신 가벼운 함수 호출만으로 전환이 가능해졌습니다!!
CPU 코어 수만큼만 스레드를 만드는 것은 cooperative thread pool (협력형 스레드 풀)이라고 불러요.
- CPU 코어 수만큼 스레드를 생성해요.
- 작업이 잠깐 멈춰도 새로운 스레드를 생성하지 않고, continuation으로 관리해요.
- 스레드가 항상 일할 수 있고, 스레드 폭발이 일어나지 않아요.
과거 GCD에서는 이를 위해서, 앱을 만들 때 기능별로 서브시스템을 만들고,
각 서브시스템마다 하나의 직렬 큐만을 사용하라고 했습니다.
서브시스템 내부에서 작업이 동시에 섞여버리면 데이터가 꼬일 위험이 있기 때문이고,
직렬 큐가 너무 많으면 위에서 말한 것처럼 스레드 폭발 위험이 있기 때문이에요.
하지만 이는, 서브 시스템 내에서 동시성(2개 이상 처리)을 거의 못하고,
병렬 처리를 못한다는 단점이 있었어요.
어쨌든...!
해당 앱의 스레드들(코어 개수만큼)이 멈추지 않는다는 런타임 계약
요 약속이 중요한데요…
코어의 개수만큼만 스레드를 유지하기 위해서는, 동시성 작업들의 안전함이 보장되어야 하고,
이러한 cooperative thread pool에서의 계약을 지키기 위해
Task Group, Actor 등등의 도구들이 만들어졌다고 해요.
이후에 해당 도구들을 공부하면서,
관련 도구들의 Behind the Scene들을 들고오도록 하겠습니다!
감사합니다.
참고
https://developer.apple.com/videos/play/wwdc2021/10254/
Swift concurrency: Behind the scenes - WWDC21 - Videos - Apple Developer
Dive into the details of Swift concurrency and discover how Swift provides greater safety from data races and thread explosion while...
developer.apple.com
https://developer.apple.com/videos/play/wwdc2021/10132/
Meet async/await in Swift - WWDC21 - Videos - Apple Developer
Swift now supports asynchronous functions — a pattern commonly known as async/await. Discover how the new syntax can make your code...
developer.apple.com
'콩코롱시의 밤' 카테고리의 다른 글
[Swift concurrency] 콩코롱시의 밤 5일차 (1) | 2025.06.24 |
---|---|
[Swift concurrency] 콩코롱시의 밤 4일차 (3) | 2025.06.20 |
[Swift concurrency] 콩코롱시의 밤 2일차 (0) | 2025.06.20 |
[Swift concurrency] 콩코롱시의 밤 1일차 (5) | 2025.06.20 |