일반적으로 std::async를 호출해서 어떤 함수나 함수 객체를 실행한다는 것은 그 함수를 비동기적으로 실행하겠다는 의도가 깔려 있다.
하지만 항상 그런 의미일 필요는 없고 std::async 호출은 함수를 어떤 시동 방침에 따라 실행한다는 좀 더 일반적인 의미를 가진다.
◾ std::launch::async
넘겨진 함수(f)는 반드시 비동기적(다른 스레드)으로 실행된다.
◾ std::launch::deferred
넘겨진 함수(f)는 std::async가 돌려준 future 객체에 대해 get이나 wait가 호출될 때에만 실행될 수 있다.
get이나 wait가 호출될 때까지 지연되며, 호출 시 f는 동기적으로 실행된다.
즉, 호출자는 f가 실행 종료될 때까지 차단되며 get이나 wait가 호출되지 않으면 f는 절대 실행되지 않는다.
std::async의 기본 시동 방침은 두 방침을 OR 연산자로 결합한 것이다.
auto fut1 = std::async(f);
auto fut2 = std::async(std::launch::async | std::launch::deferred, f);
위 둘은 완전히 같은 의미이다. 두 방침을 모두 가지고 있기 때문에 유연성을 가진다.
그런데 그 유연성 때문에 기본 방침으로 f를 실행하면 실행 시점의 예측이 불가능해진다.
auto fut = std::async(f);
예를 들어 위의 문장이 스레드 t에서 실행된다고 했을 때,
◾ f는 지연 실행될 수도 있으므로, f가 t와 동시에 실행될지 예측하는 것이 불가능하다.
◾ f가 fut에 대해 비동기적으로 실행될지 예측하는 것이 불가능하다.
◾ fut에 대한 get이나 wait 호출이 일어난다는 보장이 없을 수도 있으므로, f가 반드시 실행될 것인지 예측하는 것이 불가능하다.
이런 유연성은 만료 시한이 있는 wait 기반 루프에도 영향을 미친다. 지연된 작업에 대해 wait_for나 wait_until을 호출하면 std::future_status::deferred라는 값이 반환되기 때문이다.
using namespace std::literals;
void f() {
std::this_thread::sleep_for(1s);
}
auto fut = std::async(f);
while(fut.wait_for(100ms) != std::future_status::ready) {
...
}
f가 지연된다면 fut.wait_for는 항상 std::future_status::deferred를 반환하고 두 값은 절대 같지 않기 때문에 루프를 영원히 돌게된다. 심지어 이런 버그는 부하가 아주 많이 걸리지 않는 이상 드러나지 않아서 발견하기도 어려울 수 있다.
생각보다 이런 문제의 해결책은 간단한데, std::async 호출이 돌려준 future 객체를 이용해서 해당 작업이 지연되었는지 점검하고 만약 지연되었다면 시간 만료 기반 루프에 진입시키지 않으면 된다.
실제로 작업 지연여부를 직접 알아내는 방법은 없지만 wait_for 같은 시간 만료 기반 함수를 호출해서 반환값이 std::future_status::deferred인지만 확인하면 된다.
auto fut = std::async(f);
if (fut.wait_for(0s) == std::future_status::deferred) {
// fut에 wait나 get을 적용해서 f를 동기적으로 호출한다
}
else {
while (fut.wait_for(100ms) != std::future_status::ready) {
...
}
}
어떤 작업에 대해 기본 시동 방침과 함께 std::async를 사용하는 것은 다음 조건들이 모두 성립할 때에만 적합하다.
◾ 작업이 get이나 wait를 호출하는 스레드와 반드시 동시적으로 실행되어야 하는 것은 아니다.
◾ 여러 스레드 중 어떤 스레드의 thread_local 변수들을 읽고 쓰는지가 중요하지 않다.
◾ std::async가 돌려준 future 객체에 대해 get이나 wait가 반드시 호출된다는 보장이 있거나, 작업이 전혀 실행되지 않아도 괜찮다.
◾ 작업이 지연된 상태일 수도 있다는 점이 wait_for나 wait_until을 사용하는 코드에 반영되어 있다.
위 조건중 하나라도 충족하지 않는다면 비동기적으로 실행하도록 강제할 필요가 있다.
auto fut = std::async(std::launch::async, f);
그런데 매번 시동 방침을 지정하는 것은 번거롭다.
template<typename F, typename... Ts> // C++11
inline std::future<typename std::result_of<F(Ts...)>::type> reallyAsync(F&& f, Ts&&... params) {
return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
}
template<typename F, typename... Ts> // C++14
inline auto reallyAsync(F&& f, Ts&&... params) {
return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
}
간단하게 함수 템플릿을 만들어서 사용하면 편하게 사용할 수 있다.
◾ std::async의 기본 시동 방침은 작업의 비동기적 실행과 동기적 실행을 모두 허용한다.
◾ 그러나 이러한 유연성 때문에 thread_local 접근의 불확실성이 발생하고, 작업이 절대로 실행되지 않을 수도 있고, 시간 만료 기반 wait호출에 대한 프로그램 논리에도 영향이 미친다.
◾ 작업을 반드시 비동기적으로 실행해야 한다면 std::launch::async를 지정하라.
'도서 > Effective Modern C++' 카테고리의 다른 글
[7장] 항목 38 : 스레드 핸들 소멸자들의 다양한 행동 방식을 주의하라 (0) | 2023.01.30 |
---|---|
[7장] 항목 37 : std::thread들을 모든 경로에서 합류 불가능하게 만들어라 (0) | 2023.01.30 |
[7장] 항목 35 : 스레드 기반 프로그래밍보다 작업 기반 프로그래밍을 선호하라 (0) | 2023.01.30 |
[6장] 항목 34 : std::bind보다 람다를 선호하라 (0) | 2023.01.29 |
[6장] 항목 33 : std::forward를 통해서 전달할 auto&& 매개변수에는 decltype을 사용하라 (0) | 2023.01.27 |