← 기술 지식

기술 지식 / 프로그래밍 언어 / C++

C++ 코루틴 프레임의 소유와 최종 파괴

프레임·빌린 버퍼·클로저 수명을 나누고 await 등록 직후 완료·취소 drain·final_suspend·executor별 소멸 책임을 설명합니다.

언어·런타임 · 원리·구현·운영
기술 지식 트리

span과 원본 배열 수명의 분리

코루틴 수명은 최소 네 가지를 따로 추적해야 합니다. 프레임 메모리, 프레임 안의 값 객체, 외부에서 빌린 대상, 완료 callback이 프레임을 다시 찾는 경로입니다. 프레임이 살아 있다는 사실은 나머지 세 가지를 자동으로 연장하지 않습니다.

호출자가 지역 배열을 만들고 그 배열의 span을 비동기 쓰기 코루틴에 넘긴 뒤 반환한다고 합시다. 코루틴이 중단 상태를 프레임에 보존해도 span은 포인터와 길이를 가진 빌린 view입니다. 원래 배열의 소유권을 얻지 않으므로 이후 I/O가 배열을 읽으면 수명 위반이 됩니다.

코루틴은 중단·재개를 표현하는 언어 기능이고 버퍼 소유권·취소·스케줄링은 반환 타입과 awaiter·I/O 라이브러리의 계약입니다. 함수가 짧아졌다고 콜백의 수명 문제가 없어진 것은 아닙니다.

프레임 값과 빌린 대상의 구분

프레임에서 보이는 것 자동으로 살아 있는 것 별도로 필요한 조건
값 매개변수로 받은 소유 버퍼 그 매개변수의 소유 자원 실제 I/O 종료까지 프레임 유지
span·reference view·참조 자체 외부 대상 수명
멤버 함수의 this 객체 주소 멤버 객체의 별도 소유
캡처 있는 코루틴 람다 클로저와 프레임은 별개일 수 있음 클로저 수명 또는 값 매개변수화
coroutine_handle 프레임을 가리키는 핸들 단독·공유 소유 규약과 destroy 책임

임시 캡처 람다를 호출하고 즉시 클로저가 사라지는데 코루틴이 나중에 캡처를 읽는 구조는 특히 주의합니다. 필요한 값을 코루틴의 값 매개변수로 명확히 넘기거나 라이브러리가 보장하는 클로저 소유 방식을 사용합니다. shared_ptr로 대상 객체를 살려도 coroutine frame 자체와 I/O 버퍼가 각각 유효한지 확인해야 합니다.

await_suspend 등록과 동시 재개

완료 콜백을 await_suspend에서 등록하는 순간, 다른 worker가 그 완료를 처리해 코루틴을 재개할 수 있습니다. 그 재개 경로가 awaiter나 프레임의 수명에 영향을 준다면, 등록 호출이 아직 끝나지 않았다는 이유로 await_suspend가 해당 객체의 필드를 계속 읽거나 쓰면 안 됩니다. 등록 전에 필요한 상태를 준비하고 핸들을 게시한 뒤에는 awaiter 계약이 허용한 접근만 남깁니다.

이것은 “동기 완료면 언제나 같은 호출 스택”이라는 가정으로 해결할 수 없습니다. 즉시 완료·지연 완료·등록 실패를 각각 정의하고 resume가 동시에 두 번 실행되지 않도록 단일 완료 권위를 둡니다. 손으로 작성한 awaiter는 메모리 순서·재진입·예외까지 함께 검토해야 합니다.

취소 요청과 프레임 파괴 허가

다시는 재개되지 않을 때 프레임을 파괴합니다논리 취소 요청. I/O 종결·완료 경로 정리. 실행·재개 참조 없음 확인: 라이브러리의 안전한 정지 상태. 소유자가 destroy 한 번실제 중단 확인늦은 callback 차단파괴 허가논리 취소 요청I/O 종결·완료 경로 정리실행·재개 참조 없음 확인라이브러리의 안전한 정지 상태소유자가 destroy 한 번
다시는 재개되지 않을 때 프레임을 파괴합니다 화살표는 종료에 필요한 순서입니다. 취소 요청만으로 완료 callback의 참조가 사라지는 것은 아니므로 등록 해제 또는 drain과 실행 중 재개 종료를 확인합니다.
그림을 글로 읽기
  • 논리 취소 요청
  • I/O 종결·완료 경로 정리
  • 실행·재개 참조 없음 확인: 라이브러리의 안전한 정지 상태
  • 소유자가 destroy 한 번
  1. 논리 취소 요청 → I/O 종결·완료 경로 정리: 실제 중단 확인
  2. I/O 종결·완료 경로 정리 → 실행·재개 참조 없음 확인: 늦은 callback 차단
  3. 실행·재개 참조 없음 확인 → 소유자가 destroy 한 번: 파괴 허가

사용자 timeout으로 coroutine_handle을 즉시 destroy했다고 합시다. 늦게 도착한 I/O 완료 경로가 여전히 그 핸들을 resume하려 하면 이미 해제된 프레임을 읽게 됩니다.

취소를 요청한 주체와 실제 완료를 확인하는 주체를 구분하고, I/O callback의 등록 해제 또는 drain으로 늦은 완료 경로를 정리하고 실행 중인 재개의 종료를 확인한 뒤 종료 상태 전이를 하나의 경로로 관리합니다. 세대 검사는 잘못된 결과 적용은 막을 수 있어도 해제된 프레임 주소를 읽는 행위 자체를 안전하게 만들지 않습니다.

Final suspend 정책과 핸들 유효성

final_suspend에서 정지한 프레임은 논리 계산이 끝났어도 메모리가 남습니다. 반환 task·awaiter·scheduler 중 누가 결과를 소비하고 최종 destroy하는지 명시합니다. 소유 task를 이동하면 원본의 핸들을 비우고, 두 소멸자가 같은 프레임을 파괴하지 않게 합니다.

final_suspend가 정지하지 않는 정책에서는 완료하면서 프레임이 자동 파괴될 수 있어 외부 핸들을 계속 유효하다고 사용할 수 없습니다. 완료된 final-suspended 코루틴을 다시 resume하는 것도 허용된 일반 사용이 아닙니다. null 검사나 done 호출만으로 이미 해제된 핸들을 안전하게 조사할 수는 없습니다.

결과 소비자가 사라지는 경우, 예외 결과가 저장된 경우, continuation 재개 중 소유자가 해제되는 경우를 반환 타입의 상태 머신에 포함합니다. C++20이라는 표준 버전만으로 모든 task 라이브러리의 소유 규약이 같아지지 않습니다.

반환 타입을 선택할 때는 eager/lazy, final_suspend, move 후 원본 상태, destroy 주체, cancellation join 방식이 문서화되어 있는지 먼저 확인합니다. 소유자가 불명확한 raw coroutine_handle을 여러 계층에 복사하는 것보다 단일 RAII owner와 명시적 borrow를 두는 편이 수명 추적에 유리합니다.

소멸자 실행 executor와 자원 수명 계약

프레임을 destroy하면 프레임에 남은 객체의 소멸이 그 실행 경로에서 일어납니다. UI·장치 자원처럼 특정 executor에서 해제해야 한다면 파괴 작업을 그곳으로 보내고 실제 정리까지 소유를 유지합니다. 큐에 넣었다는 사실이 정리 완료는 아닙니다.

종료 순서는 새 작업 차단, I/O·continuation 종결, 프레임 정리 작업 완료, 정리 executor 종료가 되어야 할 수 있습니다. executor가 먼저 내려가면 정리를 실행할 곳이 없어집니다. 거절된 파괴 작업을 어느 스레드에서든 대신 실행할지, 종료를 보류할지는 해당 자원의 제약에 맞춰 명시해야 합니다.

재개·파괴 경계와 동시성 시험

테스트 awaiter로 등록 즉시 완료, 등록 실패, 취소 직후 늦은 완료를 재현합니다. 반환 task 이동, 호출자 객체 선행 파괴, final suspend 이후 소비자 소멸, executor 종료 경쟁도 포함합니다. 프레임 생성·파괴 수와 resume 중첩 여부, 버퍼 실제 종결 시점을 추적합니다.

ASan과 동시성 도구는 보조로 사용하되 단순 반복 통과를 수명 증명으로 보지 않습니다. 이 노트는 라이브러리 설계의 점검 절차이며 컴파일 가능한 범용 awaiter 구현이나 실제 실행 검증을 제공한 것은 아닙니다.

예상 결과는 등록 즉시 완료 경로에서도 awaiter가 이미 반환한 뒤 재개될 수 있고, 취소 직후 늦은 callback은 프레임을 읽지 못하도록 drain 또는 등록 해제로 차단되는 것입니다. executor 종료를 먼저 실행하면 올바른 destroy 예약도 수행되지 않으므로 종료 순서 시험에서 이를 별도 실패로 관찰합니다.

후속·관련 개념

이 개념의 확장

함께 읽을 문서

전체 기술 지식
선택 연습 · 관련 질문 3개

질문은 이해를 확인하는 보조 자료입니다. 학습 경로와 문서의 범위는 질문 수와 독립적입니다.