concurrency in action(2)
thread는 std::move를 지원한다.
thread_guard가 thread의 소유권을 가져갈 수 있다.
thread_guard에 thread의 소유권을 넘기면 호출자에서는 더이상 join()과 detach()를 못하는데 thread_guard에는 그를 위한 메서드를 정의해두지 않기 때문이다.
따라서 thread_guard는 thread보다 오래 생존할 수 없게 된다.
이는 thread_guard가 thread보다 오래 생존하는 불쾌한 상황(unpleasant consequences)을 피하게 해준다.
또한 thread_guard는 스택 오브젝트이기 때문에 thread의 소멸 시점은 호출자의 스코프가 끝날 때가 된다.
저자는 이를 scoped_thread로 이름붙였다.
아래는 예제
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class scoped_thread { std::thread t; public: explicit scoped_thread(std::thread t_): t(std::move(t_)) { if(!t.joinable()) throw std::logic_error(“No thread”); } ~scoped_thread() { t.join(); } scoped_thread(scoped_thread const&)=delete; scoped_thread& operator=(scoped_thread const&)=delete; }; struct func; void f() { int some_local_state; scoped_thread t(std::thread(func(some_local_state))); do_something_in_current_thread(); } | cs |
주목할 점이 보인다.
이전 코드에서는 t가 join가능한지 체크를 다음과 같은 방식으로 했다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | 소멸자() { if(t.joinable() == true) { t.join(); } } | cs |
주목할 점은 이번 코드는 위와 달리 t.joinable() == true 인지 생성자에서 체크하게 한 것이다.
이번 코드에서는 join과 detach를 호출하는 것이 외부에서 불가하다.
따라서 joinable()을 thread를 넘겨받아 객체를 초기화하는 생성자에서 체크해도 소멸될 때까지 그대로라는 것.
joinable()가능하지 않은 객체는 예외를 발생시킨다.
예외를 발생시키는 요소는 가급적 빨리 확인해야 하기 때문에 생성자에서 체크하는 것 같다.
move-aware(이동이 가능한) 컨테이너라면 다음과 같이 사용할 수도 있다.
1 2 3 4 5 6 7 8 9 10 11 | void do_work(unsigned id); void f() { std::vector<std::thread> threads; for(unsigned i=0;i<20;++i) { threads.push_back(std::thread(do_work,i)); } std::for_each(threads.begin(),threads.end(), std::mem_fn(&std::thread::join)); } | cs |
알고리즘 작업에서 쓰이는 스레드들은 보통, 알고리즘이 호출자로 리턴하기 전 모두 끝나야 한다.
위의 예제는 스레드의 연산결과들이 공유되는 데이터에 대한 순수한 사이드이펙트라는 것을 보여준다.
스레드의 return 값을 호출자에서 알아낼 방법이 없다는 것이다.
f가 호출자에게 알고리즘의 결과를 리턴하고 싶다면
스레드들의 실행이 모두 종료된 뒤 공유되는 데이터를 확인한 후 리턴값을 결정해야 한다.
다음은 std::accumulate를 (저자가)직접 구현한 버전이다.
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 32 33 34 35 36 37 | template<typename Iterator,typename T> struct accumulate_block { void operator()(Iterator first,Iterator last,T& result) { result=std::accumulate(first,last,result); } }; template<typename Iterator,typename T> T parallel_accumulate(Iterator first,Iterator last,T init) { unsigned long const length=std::distance(first,last); if(!length) return init; unsigned long const min_per_thread=25; unsigned long const max_threads = (length+min_per_thread-1)/min_per_thread; unsigned long const hardware_threads = std::thread::hardware_concurrency(); unsigned long const num_threads = std::min(hardware_threads!=0 ? hardware_threads:2,max_threads); unsigned long const block_size=length/num_threads; std::vector<T> results(num_threads); std::vector<std::thread> threads(num_threads-1); Iterator block_start=first; for(unsigned long i=0;i<(num_threads-1);++i) { Iterator block_end=block_start; std::advance(block_end,block_size); threads[i]=std::thread( accumulate_block<Iterator,T>(), block_start,block_end,std::ref(results[i])); block_start=block_end; } accumulate_block<Iterator,T>()( block_start,last,results[num_threads-1]); std::for_each(threads.begin(),threads.end(), std::mem_fn(&std::thread::join)); return std::accumulate(results.begin(),results.end(),init); } | cs |
당신은 하드웨어가 지원하는 것보다 더 많은 스레드를 사용하고 싶지 않을 것이다.
이를 oversubscription이라고 부른다.
32코어 cpu에서 32개 이상의 스레드를 사용하는 것이 그 예이다.
그 이상의 스레드를 사용할 경우 context switching이 성능을 감소시킬 것이다.
위 코드의 std::hardware::concurrency()는 당신의 cpu가 지원하는 스레드의 수를 리턴해준다.
따라서 이를 통해 스레드의 수를 적절히 조절할 수 있다.
만약 std::hardware::concurrency()가 0이라면 당신은 당신 임의로 스레드의 수를 선택한다.
이같은 싱글코어에서 너무 많은 스레드를 선택하지 않는 것이 좋을 것이다.
반대로 너무 적은 수의 스레드를 선택하는 것 역시 좋지 않다.
가능한 동시성을 모두 사용 못하게 되기 때문
위 코드에서 스레드의 시작점들은 각각, 연산해야 하는 숫자리스트의 길이를 나눈 것이다.
num_threads보다 1개의 적은 스레드를 생성해야 한다는 것에 주목하자.
왜냐하면 당신은 이미 한 개의 스레드를 실행 중이기 때문이다(main 함수가 실행되는 스레드)
당신은 std::vector<T>를 이용해서 알고리즘을 연산하는 함수의 중간 결과값을 저장할 수 있다.
또한 std::vector<std::thread> 를 이용해서 스레드를 생성할 수 있다.
모든 스레드를 시작시키고 나면 이제 마지막 스레드만이 남긴다.
이 곳이 당신이 나누어 떨어지지 않는 블럭을 처리해야 할 곳이다.
당신은 그 블럭이 끝에 처리되어야만 한다는 것을 알고 있다.
따라서 그 블럭 안의 원소의 개수가 어떻든 중요한 것이 아니다.
Before you leave this example, it’s worth pointing out that where the addition operator for the type T is not associative (such as for float or double), the results of this parallel_accumulate may vary from those of std::accumulate, because of the grouping of the range into blocks. Also, the requirements on the iterators are slightly more stringent: they must be at least forward iterators, whereas std::accumulate can work with single-pass input iterators, and T must be default constructible so that you can create the results vector. These sorts of requirement changes are common with parallel algorithms; by their very nature they’re different in some manner in order to make them parallel, and this has consequences on the results and requirements.
이 예제를 뜨기 앞서 T에 대한 추가 연산자가 float이나 double과 같이 서로 연관되지 않을 경우를 지적할 필요가 있다.
std::accumulate와 저자가 이 예제에서 구현한 parallel_accumulate의 결과는 아마 다를 것이다.
넘겨받은 원소의 범위를 블럭에 그룹짓는 방식 때문이다.
또한, 이 예제에서 반복자들에 대한 요구사항은 조금 더 엄격하다.
std::accumulate가 single-pass-input iterator 로 작업할 수 있는 반면
이 예제에서는 최소한 forward-iterator여야 한다.
single-pass 알고리즘에 대해 설명하자면, 한 번 참조하면 다시 참조를 못한다.
예를 들어 설명하자면 std::cout<<는 c++에서 문자열 출력 함수다.
이 함수에 문자열 인자를 넘겨준다면 함수는 문자열을 왼쪽부터 오른쪽까지 읽어나갈 것이다.
그리고 오른쪽에서 왼쪽으로 되돌아가며 읽는 일은 없을 것이다.
single-pass 알고리즘은 이럴 때 사용하는 단어로 한 번 패스한 주소는 다시 참조하지 않는다는 것이다.
즉 다시 되돌아갈 수 없다.
input iterator는 single-pass 알고리즘을 지원하는 반복자로 동등비교 연산(== !=)과 역참조연산( * ), 더하기연산( ++ )만을 지원한다.
forward-iterator는 multi-pass 알고리즘을 지원하는 반복자로 input iterator와 똑같은 인터페이스를 지원한다.
차이점은 이전에 읽은 문자를 다시 읽을 수 있느냐 없느냐 하는 것이다.
다음은 예제
1 2 3 4 5 6 7 8 9 10 11 12 | forward_iterator iter = some_list.begin(); forward_iterator iter2 = iter; item i = *iter; // Legal, we're using a first pass ++iter; // Legal, moving forward --iter; // Illegal! It's a forward-only iterator item i2 = *iter2; // Legal, we're using a second pass to read an earlier item For input iterator this would be illegal. item i2 = *iter2; //Illegal | cs |
Input Iterators in C++ - GeeksforGeeks
Input Iterators in C++ - GeeksforGeeks
A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.
www.geeksforgeeks.org
c++ - What is a Single Pass Algorithm - Stack Overflow
What is a Single Pass Algorithm
While i'm reading about STL iterators, this note found in https://www.sgi.com/tech/stl/Iterators.html The most restricted sorts of iterators are Input Iterators and Output Iterators, both of which
stackoverflow.com
갈 길이 멀다..
하다보면 오르겠지.. 번역속도
잠 안오면 딴 책 돌려야겠다.
Game Programming in c++ 이거..
저번 여름방학 때 하려다가 못했다.
지금 가만 생각해보면 내가 할 수준이 아니었다.
그러나 지금은 가능할 것 같다.
책에서 뭘 가지고 뭘 하는지 감이 약간씩은 잡히기 때문이다.
물론 초반이니까 그런 걸지도 모르겠고
후반가면 모른다.
중요한 사실은 드디어 무언가 그래픽을 만들 수단이 생겼다는 것..