await_suspend에서 std::coroutine_handle을 반환하면 대칭 전송이 가능해지며, 이는 보장된 꼬리 호출 최적화(TCO) 형태입니다. await_suspend가 void를 반환하면, 코루틴 런타임은 다음 코루틴을 재개하기 전에 호출자에게 반환해야 하므로, 체인의 길이에 따라 선형적으로 성장하는 중첩 호출 스택이 만들어집니다. 핸들을 반환함으로써 컴파일러는 타겟 코루틴의 재개 지점으로 직접 점프(jmp 명령어)할 수 있게 되어, 현재의 활성 레코드를 재사용하고 체인 길이에 관계없이 상수 O(1) 스택 깊이를 유지합니다.
struct SymmetricTransfer { std::coroutine_handle<> next; // 꼬리 호출 최적화: 스택 성장 없음 std::coroutine_handle<> await_suspend(std::coroutine_handle<>) { return next; } void await_resume() {} bool await_ready() { return false; } };
우리는 전문 음악 제작 소프트웨어를 위한 실시간 오디오 처리 엔진을 개발했습니다. 이 시스템은 500개 이상의 디지털 신호 처리(DSP) 효과(필터, 압축기, 리버브)를 연결하는 파이프라인을 나타내기 위해 C++20 코루틴을 사용했습니다. 스트레스 테스트 중, 복잡한 효과 랙을 로드할 때 애플리케이션이 스택 오버플로우로 충돌했으며, 각 개별 코루틴은 최소한의 로컬 상태를 가지고 있음에도 불구하고 이 문제가 발생했습니다.
해결책 1: 직접 재개하는 void 반환 await_suspend 초기 구현은 **void await_suspend(std::coroutine_handle<>)**를 사용하고 내부적으로 **next.resume()**를 호출했습니다. 이 접근 방식은 직관적이고 순차적인 코드 흐름을 제공하며 표준 스택 트레이스를 통해 쉽게 디버깅할 수 있었습니다. 그러나 각 resume() 호출은 이전 코루틴의 일시 정지 로직 내에 중첩되어 약 16KB를 소비하며, 500단계 후에는 8MB 스레드 스택이 소진되었습니다.
해결책 2: 비동기 스케줄링을 통한 작업 큐 우리는 각 코루틴이 다음 단계를 작업 항목으로 제출하고 즉시 일시 중지하는 중앙 집중식 작업 큐로 직접 체인을 대체하는 것을 고려했습니다. 이는 재귀를 반복으로 변환하여 상수 스택 사용을 보장했습니다. 그러나 그 대가는 성능의 현저한 저하였습니다: 큐 노드에 대한 동적 할당, 스레드 경합으로 인한 캐시 충돌, 파이프라인 단계 간의 캐시 지역성 손실로 인해 서브 밀리초 대기 시간 요구 사항이 위배되었습니다.
해결책 3: coroutine_handle을 통한 대칭 전송 우리는 await_suspend를 수정하여 다음 단계의 std::coroutine_handle을 직접 반환하도록 했습니다. 이는 컴파일러에게 TCO를 수행하도록 신호를 보내어 스택 프레임을 축소하게 했습니다. 이 솔루션은 코루틴의 제로 비용 추상을 보존하면서 O(1) 메모리 사용을 보장했습니다. 주요 위험은 생명 주기 관리와 관련되어 있었습니다: 핸들이 반환된 후 현재 코루틴은 일시 정지되었으며, 반환 지점 이후에 this 또는 로컬 변수를 접근하는 것은 정의되지 않은 동작을 초래했습니다.
선택한 솔루션 및 결과 우리는 해결책 3을 채택했습니다. 리팩토링 후, 파이프라인은 4KB의 스택 공간만 사용하여 512개의 연속 효과를 성공적으로 처리하여 충돌을 방지하고 결정론적인 실시간 성능을 유지했습니다. 이 변경은 await_suspend 내에 반환 후 로직이 존재하지 않도록 하기 위해 신중한 코드 검토가 필요했지만, 강력하고 확장 가능한 아키텍처로 이어졌습니다.
대칭 전송이 await_suspend 내에서 다음 코루틴에서 co_await를 사용하는 대신 std::coroutine_handle를 반환해야 하는 이유는 무엇인가요? await_suspend 안에서 co_await를 사용하면 대기하는 코루틴이 완전히 일시 정지된 후 나중에 재개되어야 하므로, 본질적으로 런타임으로 돌아가고 스택이 성장하게 됩니다. 핸들을 직접 반환하면 컴파일러가 재개를 꼬리 호출로 처리할 수 있게 되며, 반면 co_await는 호출자의 프레임을 유지해야 하는 비대칭 일시 정지 지점을 생성합니다.
대칭 전송이 재개된 코루틴이 최종 일시 정지 지점에 도달하기 전에 예외를 발생시키면 예외 안전성에 어떤 영향을 미치나요? 대칭적으로 전송된 코루틴이 예외를 발생시키면, 예외는 개념적으로 await_suspend 프레임을 통해 풀리지만, 원래 코루틴은 이미 일시 정지로 표시되었기 때문에, 스택 언와인딩 중에 해당 프레임을 파괴해야 합니다. 이를 위해 컴파일러는 일시 정지된 코루틴의 promise와 캡처된 매개변수를 파괴하는 복잡한 예외 처리 테이블을 생성해야 합니다. 후보자들은 종종 사용자 정의 promise_type 할당기가 부분적인 구성을 올바르게 처리해야 한다는 사실을 놓치며, 그렇지 않으면 예외 언와인딩 중 이중 파괴 버그의 위험이 있습니다.
재귀 데이터 구조에서 값을 생성하는 생성기를 구현할 때 대칭 전송 사용을 방지하는 것은 무엇인가요? 생성기는 co_yield를 사용하여 호출자에게 제어를 반환하면서 그들의 상태를 유지합니다. 대칭 전송은 원래 호출자에게 제어를 전달하지 않고 전체 체인이 완료될 때까지 다른 코루틴으로 제어를 전달합니다. 따라서 생성기는 await_suspend에서 void 또는 true를 반환하는 비대칭 일시 정지를 사용해야 하며, 이를 통해 소비자가 생성된 값을 받을 수 있고 나중에 생성기를 재개할 수 있도록 합니다.