기억해 둘 사항들
- future 객체의 소멸자는 그냥 future 객체의 자료 멤버들을 파괴할 뿐
- std::async를 통해 시동된 비지연 태스크에 대한 공유 상태를 참조하는 마지막 future 객체의 소멸자는 그 태스크가 완료될 때까지 차단됨
std::thread와 deferred되지 않는 태스크에 대한 future 객체가 소프트웨어 스레드에 대응되므로 모두 소프트웨어 스레드에 대한 핸들이라고 할 수 있지만 서로 소멸자들이 다르게 행동함
std::thread 소멸자
Modern Effective C++ 항목 37 std::thread들을 모든 경로에서 unjoinable로 만들어라
기억해 둘 사항들- 모든 경로에서 std::thread를 unjoinable하게 만들어라- 소멸 시 join 방식은 디버깅하기 어려운 문제로 이어질 수 있음- 소멸 시 detach 방식은 디버깅하기 어려운 미정의 동작으로 이
eovywjr1.tistory.com
future 소멸자
future는 어떨 때에는 암묵적으로 join을 수행하고 어떨 때에는 detach를 수행한 것과 같은 결과를 내는데 프로그램 종료도 없음
피호출자는 자신의 계산 결과를 통신 채널에 기록(보통은 std::promise 객체를 통해)하고 호출자는 future 객체를 통해서 결과를 얻음
그런데 호출자가 future 객체에 대해 get을 호출하기 전에 피호출자의 결과를 std::promise에 저장한다면 태스크 종료와 함께 파괴되고,
호출자의 future 객체에 저장한다면 해당 future를 이용해 std::shared_future(복사 가능)를 생성할 경우에 피호출자에 대응되는 다수의 future 객체 중 저장할 객체를 결정하기가 어려움
따라서 피호출자, 호출자 바깥에 있는 장소(공유 상태, shared state)에 결과를 담음
일반적으로 공유 상태는 힙 기반이지만 타입과 인터페이스 구현은 표준이 구체적으로 명시하지 않고 표준 라이브러리 작성자의 자유
std::async를 통해서 실행된 비지연 태스크에 대한 공유 상태를 참조하는 마지막 future 객체의 소멸자는 태스크가 완료될 때까지 차단됨(암묵적 join)
시스템이 default launch policy에서 async를 선택한 경우와 명시적으로 지정한 경우 모두 포함됨
future의 경우 항상 마지막 future 객체이고, std::shared_future의 경우 다른 shared_future가 공유 상태를 참조하고 있지 않은 상태
마지막 future 객체까지 소멸된다면 태스크의 결과를 받아갈 수 있는 방법이 사라지고 중간에 태스크가 종료되면 메모리 누수, 데드락, 미정의 동작 등 문제 발생
다른 모든 future 객체의 소멸자는 해당 future 객체를 파괴(암묵적 detach)
deferred 태스크를 참조하는 마지막 future 객체의 경우 deferred 태스크가 절대로 실행되지 않음
결론은 소멸자는 스레드에 대해 어떤 실행도 하지 않으며 그냥 future 객체를 파괴하고 공유 상태의 참조 횟수를 감소시킬 뿐임
이런 예외를 둔 이유는 암묵적 detach와 관련된 문제를 피하려 했으나 프로그램 종료를 피할 수 없어 암묵적 join 타협안을 선택한 것
future 객체 API는 주어진 future 객체가 std::async 호출에 의해 생긴 공유 상태를 참조하는지 판단할 수 있는 수단을 제공하지 않으므로 소멸자가 비동기적으로 실행되는 태스크의 완료를 기다리느라 차단될지를 예측하는 것은 불가능
std::vector<std::future<void>> futs; // vector 소멸자가 block될 수 있음
class Widget{ // Widget 소멸자가 block될 수 있음
private:
std::shared_future<double> fut;
};
std::packaged_task
여러 원인으로도 공유 상태가 생성될 수 있는데, 예를 들면 std::packaged_task의 사용
packaged_task는 주어진 함수를 비동기로 실행할 수 있도록 포장하는데, 함수의 결과를 공유 상태에 저장하고 공유 상태를 참조하는 future 객체를 얻을 때 std::packaged_task::get_future 함수를 사용
이 경우에는 fut가 std::async 호출로 만들어진 공유 상태를 참조하지 않는 것이 명확하므로 소멸자가 정상적으로 행동
int calcValue();
std::packaged_task<int()> pt(calcValue);
std::future<int> fut = pt.get_future();
성공적으로 생성한 std::packaged_task 객체는 임의의 스레드에서 실행할 수 있음
std::async 호출을 통해서 실행할 수 있지만 굳이 packaged_task를 생성할 이유가 없음
std::packaged_task는 복사할 수 없으므로 std::thread 생성자에 넘겨줄 때에는 반드시 오른값으로 캐스팅해야 함
std::thread t(std::move(pt));
프로그램 종료, join, detach에 관한 결정은 이미 std::thread를 조작하며 결정되기 때문에 std::packaged_task 소멸자의 특별한 행동을 고려한 코드를 작성할 필요가 없음