C++프로그래밍C++ Developer

**std::promise**가 예외 객체를 스레드 경계를 넘어 연결된 **std::future**로 전송하는 구체적인 메커니즘과 이것이 공유 상태 내에서 예외 타입의 타입 지워짐을 필요로 하는 이유를 설명하십시오.

Hintsage AI 어시스턴트로 면접 통과

질문에 대한 대답

질문의 역사.

std::futurestd::promise 기능은 C++11에 도입되어 스레드 간 비동기 결과 전송을 체계화하였습니다. 이전 접근 방식은 수동 동기화를 가진 임의의 공유 메모리에 의존하여 스레드 경계를 넘는 예외 처리를 거의 불가능하게 만들었습니다. 표준화 위원회는 작업 스레드에서 발생한 모든 예외 타입을 포착하고 저장 시점에서 예외의 정적 타입을 알지 못한 채 대기 스레드에서 신뢰성 있게 재생산할 수 있는 메커니즘을 요구했습니다.

문제.

예외 객체는 다형적이며 기본적으로 스택에 할당되지만, std::promise가 생성한 스코프를 넘겨 생존해야 합니다. std::future는 결과 타입만 템플릿화되므로 예외 타입을 포함할 수 없습니다. 게다가, 소비자 스레드는 생산자 스레드보다 더 오래살 수 있으므로 예외는 공유 소유 의미론과 함께 힙 할당 저장소에서 지속되어야 합니다.

해결책.

표준은 std::promise가 **std::current_exception()**를 사용하여 예외를 포착하도록 하여 예외를 힙으로 복사하고 타입이 제거된 핸들을 저장함으로써 암시적 타입 지워짐을 수행하도록 mandate했습니다. 공유 상태(참조 카운팅 제어 블록)는 이 std::exception_ptr를 보존하여 **std::future::get()**가 예외를 감지하고 **std::rethrow_exception()**을 사용하여 다시 던질 수 있도록 합니다.

std::promise<int> prom; auto fut = prom.get_future(); std::thread([&prom]{ try { throw std::runtime_error("작업자가 실패했습니다"); } catch(...) { prom.set_exception(std::current_exception()); } }).detach(); try { int val = fut.get(); // runtime_error를 다시 던집니다 } catch(const std::exception& e) { // 전송된 예외 처리 }

실제 상황

맥락.

분산 컴퓨팅 프레임워크는 GPUOutOfMemory 또는 CorruptInputData 예외로 인해 실패할 수 있는 이미지 분할 작업을 처리하기 위한 작업 스레드를 필요로 했습니다. 메인 스레드는 이러한 특정 예외를 수신하여 CPU ​​처리로 대체하거나 데이터 재전송을 트리거해야 했습니다.

문제 설명.

초기 시도는 std::exception_ptr를 수동으로 사용했지만 예외가 여전히 메인 스레드의 오류 큐에 참조되고 있을 때 파괴되는 수명 버그로 어려움을 겪었습니다. 개발자들은 또한 다형적 저장 중 분할(slice)이나 객체 분할(object slicing) 없이 단일 결과 컨테이너에 이질적인 예외 타입을 저장하는 데 어려움을 겪었습니다.

해결책 1: 타입이 지정된 예외 큐.

팀은 각 예외 타입에 대해 별도의 큐를 유지하는 것을 고려했습니다. 이는 타입 안전성을 제공했지만 공통 큐에서 타입 지움에 대한 std::any를 필요로 하여 상당한 오버헤드와 복잡성을 추가했습니다. 또한 소비자 스레드에서 try-catch 블록으로 예외를 자연스럽게 포착하는 기능을 손상시켰습니다.

해결책 2: 가상 예외 보유자.

그들은 템플릿화된 파생 클래스를 **std::unique_ptr<ExceptionBase>**에 저장하는 추상 ExceptionBase 클래스를 구현했습니다. 이렇게 하면 다형적 저장이 가능해지지만 스레드 간 공유 소유를 유지하기 위한 수동 복제 로직이 필요하며, 다시 던질 때 가상 디스패치 오버헤드를 도입했습니다. 맞춤형 참조 카운팅은 오류가 발생하기 쉬웠고 스스로 예외 안전성을 유지하기 어려웠습니다.

선택된 해결책과 그 이유.

팀은 내부적으로 std::promise/std::exception_ptr 메커니즘을 사용하는 std::packaged_taskstd::future를 채택했습니다. 이를 통해 표준 라이브러리가 예외 포착 및 공유 상태 수명 관리를 자동으로 처리하므로 사용자 지정 타입 지움 코드를 없앨 수 있었습니다. 선택은 유지 관리를 필요로 하지 않는 예외 안전성에 대한 필요성과 사용자 지정 기본 클래스 없이 표준 예외 처리 패턴을 지원해야 하는 요구 사항에서 비롯되었습니다.

결과.

시스템은 스레드 경계를 넘어 특정 예외 타입을 성공적으로 전파하여 메모리 누수 없이, 심지어 공격적인 스레드 풀 크기 조정 중에도 작동했습니다. 메인 스레드는 미지의 오류를 위해 기본적으로 std::exception으로 함정질 수 있으면서도 GPUOutOfMemory를 구체적으로 포착할 수 있었고, 오류 처리 로직과 스레드 동기화 간의 명확한 분리를 유지했습니다.

후보들이 종종 놓치는 점

질문: std::current_exception()는 기존 예외에 대한 포인터를 저장하는 대신 예외 객체를 복사하는 이유는 무엇입니까?

대답.

catch 블록 내의 예외 객체는 일반적으로 스택 언와인딩 중 런타임에 의해 생성된 임시 복사본입니다. 원시 포인터를 저장하는 것은 catch 블록이 종료되고 스택 프레임이 파괴되면 덜컥 위험한 참조를 생성하게 됩니다. 예외를 힙으로 복사하여 **std::current_exception()**는 객체가 발생 스레드의 스택과 독립적으로 지속되도록 보장합니다. 이 복사 작업은 또한 타입 지워짐 메커니즘을 가능하게 하여 std::exception_ptr가 타입 제거된 삭제자를 통해 객체를 관리함과 동시에 후에 정확한 원래 타입을 다시 던질 수 있는 능력을 유지할 수 있도록 합니다.

질문: std::promise는 set_value()와 set_exception() 사이의 경쟁 상태를 어떻게 방지합니까?

대답.

공유 상태에는 프라미스가 충족되었는지를 추적하는 원자 상태 플래그가 포함되어 있습니다. set_value() 또는 **set_exception()**가 호출될 때 구현은 상태를 "미충족"에서 "준비 완료"로 전환하기 위해 원자적 비교 및 교환 작업을 수행합니다. 상태가 이미 준비되면 이 작업은 std::future_errorpromise_already_satisfied를 발생시킵니다. 이 원자적 전환은 준비 상태를 관찰하는 소비자 스레드가 완전히 구성된 값 또는 예외를 보게 하여 생산자와 소비자 간의 동시 접근 중 부분적인 읽기 또는 쓰기를 방지합니다.

질문: std::exception_ptr는 왜 두 개의 std::promise 및 std::future보다 오래 지속될 수 있습니까?

대답.

std::exception_ptr는 예외 객체 자체에 대해 침입적 참조 카운팅을 사용하며, 이는 std::future/std::promise 공유 상태와 독립적입니다. 이 설계는 예외 처리 코드가 비동기 작업이 완료되고 관련된 future/promise 객체가 파괴된 후에도 오래 지속되는 로그나 오류 처리기에 오류를 저장할 수 있도록 합니다. 참조 카운팅은 마지막 std::exception_ptr가 파괴될 때까지 예외 객체가 파괴되지 않도록 보장하여 여러 비동기 작업에 걸쳐 지연 오류 보고서나 예외 집계를 지원하는 용도에 적합합니다.