7.8. 코루틴
제너레이터, 비동기, 스트림 은 같은 코루틴 lowering 을 씁니다.
7.8.1. 상태 기계 lowering
rok 의 코루틴은 stackless 이며, 컴파일러가 직접 만든 상태 기계로 lower 합니다. LLVM coroutine intrinsic (llvm.coro.*) 은 쓰지 않습니다. frame 의 레이아웃을 컴파일러가 알아야 frame 의 ScanTable 을 만들 수 있기 때문입니다. arch 별 context-switch 코드는 없으며, 모든 Tier 1 타깃에서 같습니다.
yield / async / async yield 블록
→ body 함수로 들어 올림 (HIR, 클로저처럼)
→ 상태 기계 (MIR): resume 함수 + frame 타입
→ 일반 MLIR / LLVM
yield fn / async fn 은 본문을 그런 블록으로 감싸고 래퍼 타입을 돌려주는 fn 입니다.
7.8.2. 코루틴 프레임
Frame {
state : i32 // 다음에 이어갈 suspend 지점
locals : ... // suspend 지점을 넘어 살아 있는 지역만
}
- frame 은 일반 GC 객체이며, 할당 fast / slow path (7.2) 로 만듭니다. 타입마다 frame 의 ScanTable 이 있습니다.
- 중단된 동안 frame 의 GC 참조는 ScanTable 로, resume 함수가 도는 동안의 지역은 shadow stack (7.4.1) 으로 추적합니다.
- 오래 사는 task 의 frame 은 sticky mark-bit 으로 old 가 됩니다 (7.3.2).
- 호출 결과 (
GenIter<T>,AsyncFuture<T>,AsyncStream<T>와 그Try*변형) 는 frame 을 감싼 값입니다 (4.1.11).
7.8.3. Generator
1. count() 호출 → frame 할당, 래퍼 반환.
2. next() → resume 함수가 state 에서 이어 진행. yield 에서 값과 state 를 저장하고 돌아옴.
3. 다음 next() → 저장된 state 에서 이어 진행. 끝나면 none.
호출자가 진행을 주도합니다 (for x in g { .. }).
7.8.4. Async
1. fetch() 호출 → frame 할당, Future 반환.
2. poll → resume 함수가 진행. await 한 future 가 Pending 이면 state 를 저장하고 Pending.
3. 깨움 (parking registry, I/O driver, timer) → task 가 다시 큐에 들어가고 worker 가 poll.
스케줄러와 runtime 이 진행을 주도합니다 (7.6).
7.8.5. Stream
async yield 는 둘을 합친 것입니다. 같은 frame 안에서 yield 는 값을 내고, await 는 Pending 으로 중단합니다.
7.8.6. 할당 생략
frame 은 코루틴을 부를 때마다 할당합니다. 호출자가 끝까지 진행하는 코루틴을 위한 전용 생략은 없으며, 밖으로 나가지 않는 frame 은 다른 할당처럼 없어질 수 있습니다 (7.2.12).
7.8.7. yield / await 의 lowering
| 키워드 | lowering |
|---|---|
yield expr; | state 저장 + 값 반환 (Iterator) / Ready(값) 반환 (Stream) |
await expr; | 대상 future poll. Pending 이면 state 저장 + Pending 반환 |
return expr; | 상태 기계 종료 + 결과 반환 |
7.8.8. 런타임 심볼
await 의 대기와 깨움은 rok::core::task 의 parking registry 가 맡습니다 (rok::core::task 가 rok_task_hooks_install 로 설치한 hook, 8.5.4). 런타임의 rok_coro_frame_alloc / rok_coro_park / rok_coro_unpark 는 C 와 저수준 코드용이며 컴파일러는 쓰지 않습니다 (8.5).
7.8.9. 스택 깊이
중첩된 await 는 바깥 resume 함수가 안쪽 future 의 poll 을 부르는 native 호출로 진행합니다. 그래서 await 재귀의 깊이는 native stack 에 묶이며, 매우 깊은 재귀는 stack overflow 가 됩니다 (런타임 오류). 깊은 재귀가 필요하면 task 로 나눕니다.
7.8.10. 코루틴 프레임의 수명
frame 은 다른 GC 객체처럼 도달 가능한 동안 삽니다. 진행 중인 task 는 task 큐가, 대기 중인 task 는 parking registry 가, 결과를 기다리는 쪽은 그 쪽의 frame 이 참조합니다. 어디서도 참조하지 않으면 GC 가 회수합니다. 명시적 해제는 없습니다.