[Effective Modern C++] 38. 스레드 핸들 소멸자들의 다양한 행동 방식을 주의하라
합류 가능한 스레드는 바탕 시스템의 실행 스레드에 대응된다. 스레드와 비슷한 async의 future 객체도 시스템 스레드에 대응된다. 따라서 std::thread 객체와 future객체 모두 시스템 스레드에 대한 핸들이라고 할 수 있다.
그리고 join가능 스레드를 파괴하면 프로그램이 종료된다.(항목 37 참고)
하지만 future객체의 소멸자는 어떨 때는 암묵적으로 join을 수행한 것과 같은 결과를 내고 어떨 때에는 암묵적으로 detach를 수행한 것과 같은 결과를 내지만, 프로그램이 종료되는 일은 없다.
우선, future객체는 피호출자가 결과를 호출자에게 전송하는 통신 채널의 한쪽 끝이라는 점을 주목해야 한다. 피호출자는 보통은 std::promise객체를 통해서 자신의 계산 결과를 그 통신 채널에 기록한다. 그리고 호출자는 future객체를 이용해서 그 결과를 읽는다.
그럼 이 결과는 어디에 저장되는가를 살펴보자.
1. 피호출자의 std::promise에 저장하는 경우(불가능)
호출자가 해당 future객체에 대해 get을 호출하기 전에 피호출자의 실행이 끝날 수도 있으므로, 결과를 피호출자의 std::promise에 저장 할 수는 없다. std::promise객체는 피호출자의 지역범위에 있으므로, 피호출자가 완료되면 함께 파괴되기 때문이다.
2. future객체에 저장하는 경우(불가능)
해당 std::future를 이용해서 std::shared_future를 생성할 수 있으며, 원본 std::future가 파괴된 후에도 std::shared_future를 여러 번 복사할 수 있기 때문이다.
따라서 공유 상태(shared state)라고 부르는 제3의 장소에 결과를 저장한다.
이러한 공유 상태가 중요한 것은 future객체 소멸자의 행동을 그 future객체와 연관된 공유 상태가 결정하기 때문이다.
공유 상태에 따른 future객체 소멸자의 행동
1. future객체의 소멸자는 future객체의 데이터 멤버들만 파괴한다.
비동기적으로 실행되고 있는 task의 경우 이는 바탕 스레드에 암묵적 detach를 수행하는 것과 비슷하다. 즉, 지연된 task를 참조하는 마지막 future객체의 경우 그 지연된 task가 절대로 실행되지 않음을 뜻한다.
소멸자는 바탕 스레드를 무엇과도 join 시키지 않고 detach하지 않으며, 그 무엇도 실행하지 않는다. 그냥 future객체의 데이터 멤버들을 파괴하고 공유 상태 안의 참조 카운트를 감소시킬 뿐이다.
2. std::async를 통해 launch 된 비지연 task에 대한 공유 상태를 참조하는 마지막 future객체의 소멸자는 그 task가 완료될 때까지 차단된다.
본질적으로 그런 future객체의 소멸자는 task가 비동기적으로 실행되고 있는 스레드에 대해 암묵적인 join을 수행한다.
예외는 다음 조건들을 모두 만족하는 future객체에 대해서만 일어난다.
- future객체가 std::async 호출에 의해 생성된 공유 상태를 참조한다.
- task의 시동 방침이 std::launch::async이다.(명시적이든 컴파일러가 암묵적으로 선택했든 상관없음)
- future객체가 공유 상태를 참조하는 마지막 future객체이다.(std::future 이거나, std::shared_future의 경우, 다른 미래 객체가 파괴되는 동안 같은 공유 상태를 다른 std::shared_future 가 참조하고 있으면, 파괴되는 미래 객체는 그냥 자료 멤버들만 파괴한다)
이 모든 조건이 성립할 때에만 future객체의 소멸자는 비동기적으로 실행되는 task가 완료될 때까지 소멸자의 실행이 차단된다.
이는 암묵적 detach와 관련된 문제들을 피하려 했으나, 그 방침이 어려워 암묵적 join이라는 타협안을 선택한 것이라고 보면 된다.
future객체는 std::async호출에 의해 생긴 공유 상태를 참조하는지를 판단할 수 있는 수단을 제공하지 않으므로, 임의의 future객체에 대해 그 소멸자가 비동기적으로 실행되는 task의 완료를 기다리느라 차단될 것인지를 알아내는 것은 불가능하다.
요약
- future 객체의 소멸자는 그냥 future 객체의 데이터 멤버들을 파괴할 뿐이다.
- std::async를 통해 시동(launch)된 비지연 task에 대한 공유 상태를 참조하는 마지막 future 객체의 소멸자는 그 task가 완료될 때까지 차단(blocking)된다.
'Books > Effective Modern C++' 카테고리의 다른 글
댓글
이 글 공유하기
다른 글
-
[Effective Modern C++] 40. 동시성에는 std::atomic을 사용하고, volatile은 특별한 메모리에 사용하라
[Effective Modern C++] 40. 동시성에는 std::atomic을 사용하고, volatile은 특별한 메모리에 사용하라
2022.10.23 -
[Effective Modern C++] 39. 일회성(one-shot) 사건 통신에는 void future 객체를 고려하라
[Effective Modern C++] 39. 일회성(one-shot) 사건 통신에는 void future 객체를 고려하라
2022.10.23 -
[Effective Modern C++] 23. std::move와 std::forward를 숙지하라
[Effective Modern C++] 23. std::move와 std::forward를 숙지하라
2022.09.24 -
[Effective Modern C++] 22. Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하라
[Effective Modern C++] 22. Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하라
2022.09.18