일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- operator new
- virtual function
- std::async
- UE4
- 영화 리뷰
- 언리얼
- c++
- reference
- 스마트 포인터
- 보편 참조
- 상속
- 영화
- Effective c++
- effective stl
- iterator
- universal reference
- more effective c++
- resource management class
- effective modern c++
- 게임
- 예외
- lua
- 암시적 변환
- Smart Pointer
- implicit conversion
- 오블완
- 반복자
- 참조자
- 티스토리챌린지
- exception
Archives
- Today
- Total
스토리텔링 개발자
[Effective Modern C++] 35. 태스크 기반 프로그래밍(task-based programming) 본문
Effective C++/Effective Modern C++
[Effective Modern C++] 35. 태스크 기반 프로그래밍(task-based programming)
김디트 2025. 4. 9. 11:15728x90
항목 35. 스레드 기반 프로그래밍보다 태스크 기반 프로그래밍을 선호하라
함수 비동기 호출
- doAsyncWork라는 함수를 비동기로 실행한다고 하면..
- 방법은 두 가지이다.
- std::thread 객체를 생성
- 스레드 기반(thread-based) 프로그래밍
- std::async를 사용
- 태스크 기반(task-based) 프로그래밍
- std::thread 객체를 생성
int doAsyncWork();
// 스레드 기반
std::thread t(doAsyncWork);
// 태스크 기반
auto fut = std::async(doAsyncWork);
두 방법의 차이점
- doAsyncWork는 리턴값이 있는데...
- 스레드 기반에서는 여기에 접근할 방법이 없다.
- 태스크 기반에서는 async가 리턴한 객체(future 객체)를 통해 접근할 수 있다.
- doAsyncWork가 예외를 발생시킨다면...
- 스레드 기반은 바로 죽는다.
- 태스크 기반은 future 객체로 예외에 접근할 수 있다.
- 태스크 기반 접근방식이 좀 더 높은 수준의 추상을 체현한다.
동시적 C++ 소프트웨어에서의 '스레드'의 세 가지 의미
- 하드웨어 스레드
- 실제 계산을 수행하는 스레드
- 소프트웨어 스레드
- 운영체제가 하드웨어 스레드들에서 실행되는 모든 프로세서와 일정을 관리하는데 사용하는 스레드
- os 스레드, 시스템 스레드라고도 한다.
- C++ 표준 라이브러리의 std::thread
- C++ 프로세스 안에서 std::thread 객체는 소프트웨어 스레드 핸들로 작용한다.
- 즉, null 핸들을 나타내기도 한다.(소프트웨어 스레드와 대응이 되지 않은 상태)
스레드의 예외
- 소프트웨어 스레드는 자원이 제한되어 있으므로 부족하면 예외가 발생할 수 있다.
int doAsyncWork() noexcept; // 예외를 던질 수 없더라도
std::thread t(doAsyncWork); // 사용 가능한 소프트웨어 스레드가 없다면 예외 발생!
- 해결법 1. 그냥 현재 스레드에서 doAsyncWork를 실행시킨다.
- 현재 스레드에 부하(load)가 과중하게 걸릴 수 있다.
- 만일 현재 스레드가 GUI 스레드면 사용자 입력 반응성 문제가 발생할 수 있다.
- 해결법 2. 기존의 소프트웨어 스레드가 끝나길 대기한다.
- 기존 스레드가 doAsyncWork의 작업을 기다리고 있다면 교착 상태(deadlock)에 걸릴 수 있다.
oversubscription 문제
- 실행 준비가 된 소프트웨어 스레드가 하드웨어 스레드보다 많은 상황.
- oversubscription이 발생하면
- 스레드 스케줄러는 하드웨어상의 실행 시간을 여러 조각으로 나누어서(time slice) 소프트웨어 스레드들에게 배분한다.
- 한 소프트웨어 스레드에 부여된 time slice가 끝나고 다른 소프트웨어 스레드의 time slice가 시작할 때 문맥 전환(context switch)이 수행된다.
- 이 문맥 전환은 시스템의 전반적인 스레드 관리 부담을 증가시킨다.
- 문맥 전환 시 이전 time slice와 이후 time slice가 다른 하드웨어 스레드에 있으면 이 부담은 더 커진다.
- CPU 캐시에 쓸만한 캐싱이 없으므로(cold) CPU에 다시 캐싱을 진행한다.
- 다음번에 같은 하드웨어 스레드 코어에서 실행될 가능성이 큰 기존 스레드들에 대한 CPU 캐시들이 오염된다.
- oversubscription는 피하기 어렵다.
- 소프트웨어 스레드 개수와 하드웨어 스레드 개수의 이상적인 비율이란 건 없기 때문이다.
- 문맥 전환 비율, 소프트웨어 스레드가 CPU 캐시를 얼마나 효율적으로 사용하는가 여부에도 의존한다.
- 해당 하드웨어에 맞게 응용 프로그램을 잘 커스텀하더라도, 다른 컴퓨터에서 잘 동작한다는 보장이 없다.
std::async를 사용한다면
- 이런 문제들을 떠넘기는 좋은 수단이 바로 std::async이다.
// 스레드 관리 부담을 표준 라이브러리 구현자들에게 떠넘긴다.
auto fut = std::async(doAsyncWork);
- 스레드 관리 책임을 C++ 표준 라이브러리 구현자로 옮긴다.
- std::thread나 std::async나 무슨 차이가 있을까?
- 어차피 시스템이 제공할 수 있는 것보다 많은 소프트웨어 스레드를 요청한다는 근본적인 문제가 있는데...
- std::async는 이런 상황에서 그 함수를 결과가 필요한 스레드에서 실행하라고 스케줄러에게 요청할 수 있다.
- 하지만 이 기법을 적용하면 앞서 말한 부하 불균형(load balancing)이 생길 수 있다.
- 하지만 스케줄러가 독자보다 그 문제들을 더 상세히 알고 있을 가능성이 크다.
- GUI 스레드 반응성 문제 역시 여전할 수 있다.
- 스케줄러로서는 반응성이 좋아야 하는 스레드가 뭔지 알 수 없다.
- std::launch::async라는 시동 방침(launch policy)를 std::async에 넘겨줄 수 있다.
- 그러면 현재 스레드와 다른 스레드에서 실행된다.(항목 36 참조)
- 최신 스레드 스케줄러는 여러 새로운 방식으로 개선되고 있다.
- 태스크 방식은 이 업데이트를 지속적으로 받을 수 있지만
- std::thread를 직접 다루는 경우에는 독자가 직접 그를 반영해야 한다.
스레드 기반 프로그래밍이 필요한 경우
- 바탕 스레드 적용 라이브러리의 API에 접근해야 하는 경우
- C++ 동시성 API는 저수준 플랫폼 고유 API를 이용해서 구현된다.
- 이를 위해 std::thread 객체는 native_handle이라는 멤버 함수를 제공한다.
- std::future에는 이에 해당하는 기능이 없다.
- 응용 프로그램의 스레드 사용량을 최적화해야 하는, 그리고 할 수 있어야 하는 경우
- 즉, 하드웨어 특성들이 미리 정해진 컴퓨터만을 위한 최적화가 필요한 경우.
- C++ 동시성 API가 제공하는 것 이상의 스레드 적용 기술을 구현해야 하는 경우
- 이를테면 스레드 풀을 제공하지 않는 특정 플랫폼을 위해 스레드 풀을 직접 구현해야 하는 경우.
728x90
'Effective C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 37. std::thread는 unjoinable하게 (0) | 2025.04.11 |
---|---|
[Effective Modern C++] 36. std::async의 시동 방침(launch policy) (0) | 2025.04.10 |
[Effective Modern C++] 34. std::bind 대신 람다 (0) | 2025.04.08 |
[Effective Modern C++] 33. auto&& 매개변수를 std::forward로 전달할 땐 decltype (0) | 2025.04.04 |
[Effective Modern C++] 32. 람다 초기화 캡쳐(init capture) (0) | 2025.04.03 |
Comments