KoreanFoodie's Study

Effective Modern C++ | 항목 35 : 스레드 기반 프로그래밍보다 과제 기반 프로그래밍을 선호하라 본문

Tutorials/C++ : Advanced

Effective Modern C++ | 항목 35 : 스레드 기반 프로그래밍보다 과제 기반 프로그래밍을 선호하라

GoldGiver 2022. 10. 26. 10:06

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 35 : 스레드 기반 프로그래밍보다 과제 기반 프로그래밍을 선호하라

핵심 :

1. std::thread API 에서는 비동기적으로 실행된 함수의 반환값을 직접 얻을 수 없으며, 만일 그런 함수가 예외를 던지면 프로그램이 종료된다.
2. 스레드 기반 프로그래밍에서는 스레드 고갈, 과다구독, 부하 균형화, 새 플랫폼으로의 적응을 독자가 직접 처리해야 한다.
3. std::async 와 기본 시동 방침을 이용한 과제 기반 프로그래밍은 그런 대부분의 문제를 알아서 처리해준다.

 

doAsyncWork 라는 함수를 비동기적으로 실행한다고 하자. 방법은 크게 두 가지이다. 하나는 std::thread 객체를 생성해 그 객체에서 doAsyncWork 를 실행하는 것으로, 이는 스레드 기반(thread-based) 프로그래밍에 해당한다.

int doAsyncWork();

std::thread t(doAsyncWork);

또 하나는 과제 기반 프로그래밍으로, doAsyncWork 를 std::async 에 넘겨주는 것이다.

auto fut = std::async(doAsyncWork);;

이러한 호출에서, std::async 로 전달된 함수 객체(여기서는 doAsyncWork)는 하나의 과제(task) 로 간주된다.

대체로 과제 기반 접근방식이 스레드 기반 접근 방식보다 우월하다. 과제 기반 접근 방식에서는 doAsyncWork 의 반환값에 간단히 접근할 수 있으며, 더 높은 수준의 추상을 체현한다. 먼저, C++ 소프트웨어에서 '스레드' 라는 용어가 세 가지 의미로 쓰인다는 점을 지적하고 넘어가도록 하겠다.

  • 하드웨어 스레드 : 실제 계산을 수행, 현세대의 컴퓨터는 CPU 코어당 하나 이상의 하드웨어 스레드 제공
  • 소프트웨어 스레드(OS 스레드, 시스템 스레드) : 운영체제가 하드웨어 스레드들에서 실행되는 모든 프로세서와 일정을 관리하는 데 사용. 하드웨어 스레드보다 많은 소프트웨어 스레드를 생성할 수 있다.  한 소프트웨어 스레드가 차단(blocking) 되어도, 차단되지 않은 다른 소프트웨어 스레드들을 실행함으로써 산출량을 향상할 수 있다.
  • C++ 표준 라이브러리의 std::thread : 하나의 C++ 프로세스 안에서 std::thread 객체는 바탕 소프트웨어 스레드에 대한 핸들로 작용한다. std::thread 객체가 '널(null)' 핸들을 나타내기도 한다.

소프트웨어 스레드는 제한된 자원으로, 시스템이 제공할 수 있는 것보다 많은 소프트웨어 스레드를 생성하려 하면 std::system_error 예외가 발생한다. 이는 스레드에서 실행하고자 하는 함수가 예외를 던질 수 없는 경우에도 마찬가지이다. 예를 들어, 다음처럼 noexcept 가 쓰여도 예외를 던질 수 있다.

int doAsyncWork() noexcept;

// 사용 가능한 스레드가 없으면 예외가 발생한다
std::thread t(doAsyncWork);

 

과다구독(oversubscription) 때문에 문제가 발생할 수 있다. 과다구독이란 실행 준비가 된(즉, 차단되지 않은) 소프트웨어 스레드가 하드웨어 스레드보다 많은 상황을 가리킨다. 과도한 컨텍스트 스위칭(context switching) 때문에 효율이 떨어질 수도 있으며, 다른 작업을 수행하는 여러 스레드가 같은 CPU를 사용함으로써 CPU 캐시를 '오염' 시키는 경우가 발생할 수도 있다.

std::async 를 사용하면 이러한 스레드 관리 부담을 표준 라이브러리 구현자들에게 떠넘길 수 있다.

auto fut = std::async(doAsyncWork);

이렇게 하면, 가용 스레드 부족 때문에 예외를 받을 가능성이 크게 줄어든다. 이 호출은 예외를 방출할 가능성이 거의 없기 때문이다. 그런데 이것이 어떻게 가능할까?

std::async 는 새 소프트웨어 스레드를 생성하지 않을 수도 있다. 대신 std::async 는 지정된 함수를 doAsyncWork 의 결과가 필요한 스레드(즉, fut 에 대해 get 이나 wait 를 호출하는 스레드) 에서 실행하라고 스케줄러에게 요청할 수도 있다.

물론 이 방법이 만병통치약인 것은 아니다. 다만, 스케줄러가 로드 밸런싱(load balancing) 문제와 관련하여, 프로그래머보다 더 잘 알고 있을 것이라고 기대하는 것이다.

std::async 에서도 GUI 스레드의 반응성이 여전히 문제가 될 수는 있다. 그런 경우에는 std::launch::async 라는 시동 방침(launch policy) 를 std::async 에 넘겨주는 것이 바람직하다. 그러면 실행하고자 하는 함수가 실제로 현재 스레드와는 다른 스레드에서 실행된다.

따라서, 다음과 같이 스레드를 직접 다루는 게 적합한 일부 경우를 제외하면 과제 기반 설계를 선호하도록 하자.

  • 바탕 스레드 적용 라이브러리의 API 에 접근해야 하는 경우 : pthreads 라이브러리나 Windows 스레드 라이브러리의 API 는 풍부한 기능을 제공하고, std::thread 는 native_handle 이라는 멤버 함수를 제공한다. 그런데 std::future 에는 이런 기능이 없다.
  • 응용 프로그램의 스레드 사용량을 최적화해야 하는, 그리고 할 수 있어야 하는 경우
  • C++ 동시성 API 가 제공하는 것 이상의 스레드 적용 기술을 구현해야 하는 경우
Comments