ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] std::async의 launch policy (std::launch)
    C++/Effective Modern C++ 2023. 3. 28. 14:33

    지난 글 (std::thread와 std::async)에서 동시성 프로그래밍을 위해 사용되는 두 기능을 살펴보았습니다.

    몇몇 예외 상황을 제외하고, 여러 편의성을 이유로 std::async가 더 좋다는 내용이었습니다.

     

    지난 글의 예제에서는 std::async를 다음과 같이 호출했습니다.

    auto fut = std::async(function);

    하지만 이런 호출에 대하여, function함수가 비동기적으로 바로 실행된다는 보장이 없습니다.

    이번 글에서는 std::async의 작동 방식에 대하여 살펴보도록 하겠습니다.

     


     

    1. std::launch

     

    더보기

    std::async는 의미론적으로는 지정 객체를 비동기적으로 호출하는 의미이지만, 실제로는 그렇지 않습니다.

    std::async는 특정한 실행 방침 (launch policy)에 따라 객체(함수)를 호출합니다.

    이 때 지정하는 launch policy는 std::launch라는 enum class에 정의되어 있고, 그는 다음과 같습니다.

    enum class launch {
        async    = 0x1,
        deferred = 0x2
    };
    • std::launch::async는 비동기적으로 지정 객체(함수)를 호출합니다.
    • std::launch::deferred는 std::async가 반환한 std;:future객체의 get 혹은 wait함수가 실행될 때 까지 대기합니다.

    지정된 launch policy에 따라 std::async는 완전히 다른 방식으로 작동합니다.

     

    2. std::async의 기본 실행 방식

     

    더보기

    대부분의 상황에서, 우리는 비동기적인 실행을 기대하고 std::async를 사용할 것 입니다.

    하지만 std::async의 기본 실행 방식은 비동기 실행이 아닙니다.

     

    다음은 std::async의 오버로드 리스트 입니다.

    template<class T, class... ArgT>
    future<T, ArgT> async(T&& cls, ArgT&&... Args);
    
    template<class T, class... ArgT>
    future<T, ArgT> async(launch launch, T&& cls, ArgT&&... Args);

    공식 구현의 표현 중 불필요한 부분을 제외했습니다.

    간단하게 보면, std::async는 std::launch를 받는 버전과 받지 않는 버전이 있습니다.

     

    launch를 받지 않는 버전의 함수의 구현을 살펴보도록 하겠습니다.

    template<class T, class... ArgT>
    future<T, ArgT> async(T&& cls, ArgT&&... Args) {
        return async(
            launch::async | launch::deferred, 
            forward<T>(cls), 
            forward<ArgT>(Args)...
        );
    }

    launch를 받지 않는 구현은, std::launch::async와 std::launch::defeered의 or연산으로 std::async를 호출합니다.

    따라서, 기본 실행 방식은 비동기가 될 수도, 지연이 될 수도 있습니다.

     

    이는 단점만 가지고 있지는 않습니다.

    이러한 유연함이 std::async의 편의성을 만들어주는 요인이기 때문입니다.

    하지만 이러한 유연성이 부정적으로 작용하는 상황도 존재합니다.

    다음 문단에서 이어집니다.

     

    3. 기본 실행 방식의 유연성

     

    더보기

    다음과 같은 코드가 스레드 t에서 실행된다고 가정하겠습니다.

    auto fut = std::async(f);

    이 코드에 대하여 다음과 같은 추론이 가능합니다.

    • f가 t와 동시에 실행되는 것을 예측할 수 없습니다. (f가 지연실행 될 수 있습니다.)
    • f가 t와 다른 스레드에서 실행되는지 예측할 수 없습니다. (get이나 wait이 호출되는 스레드를 예측할 수 없습니다.)
    • f가 반드시 실행될 것인지 예측할 수 없습니다. (get이나 wait이 반드시 호출된다는 보장이 없습니다.)

    기본 실행 방식은 유연성을 보장하지만, 위와 같은 불확정성이 존재합니다.

    특히 스레드의 경우, 스레드 로컬 저장소(TLS)에 접근하는 코드가 있다고 할 때에는 위와 같이 스레드의 동일 여부가 매우 중요해집니다.

     

    그 외에도, 실행 여부를 예측할 수 없는 것 또한 문제 상황을 만들 수 있습니다.

    이를 단적으로 보여줄 수 있는 예제가 있습니다.

    using namespace std::literals;
    
    void f() { std::this_thread::sleep_for(1s); }
    
    auto fut = std::aysnc(f);
    
    while(fut.wait_for(100ms) != std::future_state::ready) {
        ...
    }

    위 코드는 f의 실행이 끝날 때 까지 while문을 반복하는 코드입니다.

    하지만 위와 같이 f를 인자로 fut객체를 생성했을 경우, fut가 곧바로 비동기적으로 실행된다는 보장이 없습니다.

    wait_for함수의 반환값은 아래와 같습니다.

    • std::future_state::ready - 값이 반환되어 준비된 상태입니다.
    • std::future_state::timeout - wait_for의 대기 시간동안 값이 반환되지 않았습니다.
    • std::future_state::deferred - 실행이 지연된 future객체의 상태입니다.

    f가 비동기적으로 실행되면 위 코드의 wait_for함수는 timeout을 반복하다가 ready가 출력될 것 입니다.

    하지만 defeered일 경우, get혹은 wait함수가 호출되지 않았기 때문에 무한루프가 발생합니다.

     

    위 코드의 해결은 std::future_state::deferred에 대한 확인을 선행적으로 하는 것으로 해결됩니다.

    하지만 해결책과 예외로, 기본 실행 방식을 이용하는 데 고려해야 할 점이 있다는 문제는 해결되지 않았습니다. 

     

    4. 기본 실행 방식의 사용

     

    더보기

    std::async의 호출에 있어 기본 실행 방식을 사용할 때, 위와 같은 상황이 발생할 수 있음을 알아보았습니다.

    따라서 기본 실행 방식을 사용할 때에는 다음과 같은 항목을 고려해봄이 바람직합니다.

    • 과제(Task)가 get혹은 wait을 호출하는 스레드와 동시 실행될 필요성이 없습니다.
    • 특정 스레드의 로컬 변수에 대한 접근 여부가 중요하지 않습니다.
    • std::future객체에 대한 get혹은 wait함수가 반드시 호출된다는 보장이 존재하거나, 전혀 실행되지 않아도 괜찮습니다.
    • 과제(Task)의 지연 여부가 wait_for, wait_until코드의 사용에 있어 고려되고 있습니다.

    위 경우가 아니라면, std::async를 호출할 때에는 std::launch::async를 인자로 지정하는 것이 바람직합니다.

    auto fut = std::async(std::launch::async, f);

     

    위와 같은 코드를 여러 번 사용한다면, std::async를 Wrapping하는 것도 좋은 선택이 될 수 있습니다.

    template<typename F, typename... Ts>
    inline auto reallyAsync(F&& f, Ts&&... params) {
        return std::async(
            std::launch::async,
            std::forward<F>(f),
            std::forward<Ts>(params)...);
    }

    그리고 이는 위 문단에서 확인한, 기본 실행 방식을 사용하는 std::async의 구현과 유사합니다.

     


     

    전체적으로, 유연성을 보장하기 위해 불확실성이 증가한 경우 같습니다.

    하지만 조금만 신경쓴다면 의도하지 않은 행동이 일어나지도 않을 것이고, 코드의 의도도 명확해질 것이라 생각합니다.

     

    이 글이 도움이 되셨기를 바랍니다.

    감사합니다.

    댓글

Designed by Tistory.