ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] std::thraed와 std::async
    C++/Effective Modern C++ 2023. 3. 23. 12:31

    std::thread와 std::async는 동시성 프로그래밍을 위해 사용되는 기능입니다.

    각각 아래와 같이 사용할 수 있습니다.

    더보기
    #include <iostream>
    #include <thread>
    #include <future>
    
    void test(int num) {
        while (1) {
            std::cout << num;
        }
    }
    
    int main()
    {
        // Thread
        std::thread th(test, 5);
        th.join();
        
        // Async
        auto asy = std::async(test, 7);
    }

    위 코드를 실행할 경우, 5와 7이 번갈아가며 출력됩니다.

    출력이 바뀌는 주기와 순서 등은 CPU에 의해 결정됩니다.

     

    이 두 가지 기능은, 몇 가지 이유로 인해 std::async가 std::thread보다 선호된다고 합니다.

    이번 글에서는 그 이유에 대해 알아보도록 하겠습니다.

     


     

    1. 반환값 사용

     

    더보기

    동시적으로 수행되는 특정 함수의 반환값이 항상 void일 수는 없습니다.

    하지만, std::thread에서는 함수의 반환값을 직접 가져올 수 있는 수단이 없습니다.

    이에 대한 우회책이 존재하는데, 이는 Stackoverflow의 글을 인용하도록 하겠습니다.

    C++: Simple return value from std::thread?

    이 글의 답변에 이용된 방법은 크게 두 가지로 나뉘어집니다.

    • std::future를 이용하기
    • 반환값을 참조 파라미터로 전달하기

    첫 번째 방법은 합리적으로 보이지만 이는 결과적으로 std::thread가 아닌 std:async를 이용하는 방법입니다.

    두 번째 방법은 단순한 우회책으로써, 함수의 반환값에 접근하지 못한다는 근본적인 문제가 있습니다.

     

    이에 비해, std::future객체는 함수의 반환값에 접근할 수 있는 get이라는 멤버 함수가 존재합니다.

     

    반환값 접근 외에도, 예외에 관해 중요한 차이가 존재합니다.

    std::thread를 통해 실행한 함수에서 예외가 발생할 경우, std::thread는 std::terminate 호출을 통해 프로그램을 강제 종료시킵니다.

    하지만 std::future객체의 get함수는 예외에도 접근할 수 있습니다.

     

    2. 스레드 균형화 (Load balancing)

     

    더보기

    스레드는 제한된 자원입니다.

    이번 문단에서 사용되는 스레드는 구체적으로 소프트웨어 스레드, 즉 OS에 의해 관리되는 스레드 입니다.

     

    시스템이 제공하는 것 이상의 스레드를 생성하려 할 경우, std::system_error 예외가 발생합니다.

    이는 noexcept선언이 된 함수도 마찬가지입니다.

     

    잘 만들어진 소프트웨어는 이런 상황을 방지해야 합니다.

    구체적으로는 다음과 같은 방안을 생각해볼 수 있습니다.

    • 새 스레드를 현재 스레드에서 생성합니다.
    • 기존 스레드의 종료를 대기하다 새 스레드를 생성합니다.

    첫 번째 방법은 기존 스레드에 과도한 부하가 발생할 수 있습니다.

    두 번째 방법은 기존 스레드가 새 스레드와 동시에 실행되어야 할 경우 문제가 발생할 수 있습니다.

     

    위의 스레드 생성 제한 외에도, 과다구독(Oversubscription)이라는 현상이 발생할 수 있습니다.

    이는 실행 준비가 된 소프트웨어 스레드의 숫자가 하드웨어 스레드보다 많은 상황을 가리키는 말로, 이 상황이 발생하면 스레드 스케쥴러(주로 OS)가 하드웨어 실행시간을 소프트웨어 스레드에 배분합니다.

    이 때 소프트웨어 스레드 사이의 교체가 이루어지는 Context switching이 수행되는데, 이는 시스템의 스레드 관리 부담을 증가시킵니다.

    또한, Context switching의 비용은 하드웨어 스레드에 따라 달라집니다. (이전 하드웨어 스레드와 다른 스레드일 경우 증가합니다.)

    이러한 Oversubscription현상을 피하는 것은 어렵습니다.

    소프트웨어 스레드와 하드웨어 스레드의 이상적인 비율이 동적으로 변할 수 있기 때문입니다.

     

    std::thread는 위와 같은 스케쥴링을 사용자가 직접적으로 관리해야 합니다.

    반면, std::async는 라이브러리 구현자에 의해 관리되어, 사용자에게 직접적으로 가해지는 스레드 관리에 의한 부담이 줄어들게 됩니다.

     

    3. 예외 : 스레드 기반 프로그래밍이 적합한 경우

     

    더보기

    몇 가지 예외사항에 한해, 과제 기반 프로그래밍 (std::async를 통한 std::future객체를 이용하는)에 비해 스레드 기반 프로그래밍(std::thread를 이용하는)이 적합할 수 있습니다.

     

    • 저수준의 API를 이용해야 하는 경우

    구체적으로 native_handle이라는, 스레드의 핸들에 접근할 수 있는 멤버 함수는 std::future객체에는 존재하지 않습니다. 이 외에도 pthreads 혹은 Windows 스레드 라이브러리 등의 저수준 API에서 제공하는 다른 기능들을 이용하기 위해서 std::thread를 사용해야 하는 상황이 있을 수 있습니다.

    • 스레드 사용량의 최적화가 필요한 경우

    하드웨어 특성이 미리 정해져 있는 프로세스 등의, 구체적인 최적화가 필요한 상황에서 std::thread가 사용될 수 있습니다.

    • C++의 API가 제공하는 것 이상의 스레드 적용 기술을 구현해야 하는 경우

    예를 들면, 특정 플랫폼의 C++구현이 스레드 풀을 제공하지 않는 경우 등, 스레드의 세부 기능을 직접 정의해야 하는 경우 std::thread를 사용해야 할 수 있습니다.

     


     

    전반적으로 std::async를 이용한 비동기 구현이 std::thread보다 편한 것으로 보입니다.

    하지만 std::thread를 이용할 경우 특정 상황에 맞는 세부적인 구현이 가능합니다.

     

    프로그램의 동시성 제어와 관련해서는 이 외에도 여러 주의사항이 존재합니다.

    이번 글에서는 std::thread와 std::async를 비교하여, std::async가 사용성이 더 편하다는 것을 중심으로 살펴보았습니다.

    다음 글에서는 두 API의 비교가 아닌, 각각의 API를 사용할 때, 특정 상황에서 주의해야 할 사항에 대하여 살펴보도록 하겠습니다.

     

    감사합니다.

    댓글

Designed by Tistory.