[Effective Modern C++] std::future의 소멸자
std::thread인스턴스는 소멸자 호출 시, 인스턴스의 join여부에 따라 행동이 갈라집니다.
이에 대한 자세한 내용은 이전 글 (std::thread를 unjoinable하게 만들어야 하는 이유)에 서술되어 있습니다.
joinable한 std::thread인스턴스의 소멸자가 호출되면 프로그램이 종료되며, 이는 이것이 가장 최선의 대응이기 때문이라는 내용이었습니다.
이번 글은 std::thread외에 동시성 프로그래밍에 사용되는 std::future객체의 소멸자에 대하여 살펴보도록 하겠습니다.
1. std::future의 결과 저장
std::future객체는 피호출자 (std::future객체)가 호출자(std::async등의 함수를 지정하는 코드)에게 결과를 전송하는 통신 채널이라고 볼 수 있습니다.
이 때 계산 결과는 std::promise객체를 통해, 해당 채널에 저장합니다.
추상적으로 아래와 같이 그려집니다.

이 "채널에 저장"된다는 부분을 자세히 살펴보도록 하겠습니다.
구체적으로 저장될 수 있는 곳은 아래와 같습니다.
- std::promise객체 : std::future인스턴스의 get이 호출되기 전에 피호출자의 실행이 끝날 수 있고, 이 경우에 std::promise는 실행이 종료되면 파괴됩니다. 따라서 부적합 합니다.
- std::future객체 : std::future객체를 이용해서 std::shared_future객체를 생성할 경우, 결과를 공유하는 std::shared_future객체가 여럿이 될 수 있습니다. 이 경우 어느 객체에 결과를 담아야 할 지 모호해집니다.
결과적으로 호출자도, 피호출자도 결과를 저장하기에 적합하지 않습니다.
표준은 이러한 상황에 대하여, 호출자와 피호출자 사이에 결과를 공유하는 shared state를 제시합니다.

이러한 shared state의 인터페이스, 구현은 표준이 제시하지 않습니다.
라이브러리 작성자는 shared state를 원하는 방식으로 구현할 수 있습니다.
2. future객체의 소멸자
위 문단의 결과가 중요한 이유는, future객체의 소멸자의 행동과 연관이 있기 때문입니다.
future객체의 소멸자의 행동은 다음과 같습니다.
- std::async를 통해 시동되었을 경우, 암묵적 join과 유사하게 작동합니다. (해당 실행이 종료될 때 까지 소멸자의 실행이 차단됩니다.)
- 그 외의 경우, 암묵적 detach와 유사하게 작동합니다. 실행이 지연되었을 경우, 해당 과제는 실행되지 않습니다. (future객체가 파괴됩니다.)
기본적으로, 소멸자의 역할은 자원 및 객체의 해제, 파괴입니다.
그런 관점에서 보면, 소멸자의 실행이 차단되는 상황은 비 정상적입니다.
조금 더 깊게 살펴본다면, 소멸자 호출 시 정상적인 행동(객체의 파괴)을 일으키지 않는 조건은 다음과 같습니다.
- future객체가 std::async호출에 의해 생성된 공유 상태를 참조합니다.
- 과제의 시동 방침이 std::launch::async입니다. (이는 기본 실행 방침으로 실행되어, 시스템이 선택한 async도 포함됩니다.) launch policy에 대한 내용은 다른 글 (std::async의 launch policy)에 서술되어 있습니다.
- future객체가 마지막 객체입니다. 이는 std::future에 대하여는 항상 성립하고, std::shared_future에 대하여는 참조 횟수를 확인합니다.)
위 조건을 모두 만족하는 future객체에 대하여, 소멸자의 실행이 차단됩니다. 이는 암묵적으로 join을 호출하는 것으로 볼 수도 있습니다.
동시성 프로그래밍에서 신경써야 할 부분 중, future객체의 소멸자에 대한 내용을 살펴봤습니다.
이번 글이 future객체의 이해에 도움이 되었으면 좋겠습니다.
감사합니다.