Skip to content

Chapter 04, Synchronizing concurrent operations

Dong Ha. Park edited this page Nov 12, 2015 · 5 revisions

Chap 04 : 병렬적인 연산들의 동기화

Synchronizing concurrent operations에 대한 번역.

현재 번역은 '진행 중'입니다.


1. 특정 이벤트 또는 조건 대기

Section 4.1 : Waiting for Event
On-work

2. future를 사용한 1회성 이벤트 대기

Section 4.2 : Waiting for One-off events with futures
On-work

3. 시간 제한을 이용한 대기

Section 4.3 : Waiting with a time limit
교정 대기 중

앞서 언급한 블럭 함수들은 이벤트가 발생할때까지 스레드를 무기한 정지시킵니다. 대부분의 경우 이건 문제가 되지 않죠. 하지만 몇몇 경우에, 얼마나 기다릴 것인가 라는 시간제한을 두고 싶을 수 있을 겁니다. 이런 기능이 있다면 "난 아직 살아있어" 같은 형태의 프로세스 상호간 메세징이나, 유저가 기다리는 것을 포기하고 Cancel버튼을 눌렀을때 종료시키는 것도 가능할 겁니다.

timeout(시간제한)을 두는데는 2가지 방법이 있습니다. 일정한 시간동안 대기하는 duration-based 시간제한과, 또는 특정한 시점까지 대기하는 absolute 시간제한이죠. 대부분의 waiting 함수들은 다양한 형태로 두 시간제한을 다룰 수 있게 해줍니다. _for로 끝나는 함수는 duration-based timeout 이고, _until로 끝나면 absolute timeout이죠.

예컨대, std::condition_variable이 두 가지의 재정의된(overload) 버전을 가지고 있다고 해봅시다. wait()을 오버로드한 wait_for()와, wait_until()로 말이죠. 이 함수들에는 전제(supplied predicate)가 있어서, 기한이 다되거나, 지정된 시점에 도달하거나, 가짜(spurious) wakeup이 발생하면 이 전제를 확인합니다. 전제조건을 검사한 결과가 true일 때만 반환하게 되죠.

timeout을 사용하는 함수들을 자세히 보기에 앞서, C++에서는 시간이 어떻게 명세되어 있는지 확인해봅시다. Clock부터 시작할까요?

  • Clocks(시계)

C++ 표준 라이브러리에서, clock은 시간정보를 표현하는데 사용됩니다. Clock 클래스는 4가지 정보를 가지고 있습니다.

  • 특정시간 : Now
  • clock이 소유하는 Time값을 위한 Type
  • tick period (짧은 시간 주기)
  • 일정하다고(steady) 여겨지는 시간.

현재시각은 정적 멤버함수인 now()를 호출해서 얻을 수 있습니다. 가령, std::chrono::system_clock::now()system_clock의 현재시간을 반환하죠. 이 시점(time point)의 타입은 time_point 멤버로 정해져 있습니다. 그러니 some_clock::now()의 타입은 some_clock::time_point 인거죠.

tick을 문맥상의 의미인 '시간 측정'으로 번역하였습니다.

측정 주기(tick period)는 '초'단위의 '분수'로 정의됩니다. 이 분수는 clock의 period멤버 typedef에 전달되죠. 가령, 초당 25회 측정되는 시계의 주기는 std::ratio<1,25>로 표기됩니다. 반면에 2.5초마다 1회 측정되는 경우는 std::ratio<5,2>로 표시되죠. 만약 측정 주기가 실행시간(runtime)까지 알 수 없거나, 프로그램(Application)의 실행 도중에 달라질 수 있으면, 측정 주기(tick period)는 가능한 가장 짧은 측정 주기의 평균값으로 결정됩니다. 또는 라이브러리의 작성자가 적합하다고 지정한 값을 가지게 되죠. 관측된(observed) 측정 주기가 정의된 주기와 일치한다는 것은 보장할 수 없습니다.

만약 clock이 균일하게 측정된다면, (period와 맞는지는 제쳐두고) 그리고 변경 불가능하다면, 그 clock은 일정(steady) 하다고 할 수 있습니다. is_steady 정적 데이터멤버는 이 경우 true값이며, 일정하지 않은 경우에는 false값을 가집니다. 일반적으로, std::chrono::system_clock은 균일하지 않습니다. 왜냐하면 시계가 조정될 수 있기 때문이죠. (even if sync adjustment is done automatically to take account of local clock drift.) 이런 조정은 now()를 호출하고 이전의 now()호출보다 더 이른시각 값을 반환할 수 있습니다. 이건 균일한 측정 비율(tick rate)라는 요구사항에 위배되죠. Steady clock은 시간제한(timeout) 계산에 중요합니다. 곧 보겠지만, C++ 표준 라이브러리는 std::chrono::steady_clock의 형태로 이것을 제공하고 있죠. 다른 시계는 C++표준 라이브러리의 std::chrono::system_clock(앞서 언급한)으로 제공됩니다. system_clock은 system의 '실제 시간'을 의미하고 시점(time point)을 time_t로 변환하는 함수와 std::chrono::high_resolution_clock (가능한 최소, 그렇기 때문에 최대 정밀도를 지니는 측정 주기(tick period)를 제공하는)을 지원하죠. (high_resolution_clock은 라이브러리가 지원하는 모든 tick period중 가장 작습니다.) 이건 실제로는 그냥 다른 clock의 typedef일수도 있습니다. 이 clock들은 라이브러리 헤더에 정의되어 있죠.

time point에 대한 표현을 짧게 볼 것이지만, 먼저 duration(기간)이 어떻게 표현되는지 확인해봅시다.

  • Duration(기간)

duration은 '시간'을 지원하는 부분들 중 가장 단순합니다. std::chrono::duration<> 클래스 템플릿(Thread 라이브러리에서 사용하는 C++ 시간조작 기능들은 전부 std::chrono 네임스페이스에 있습니다.)에 의해 다뤄지죠. 첫번째 템플릿 파라미터는 표현의 타입입니다. (int, long, double) 그리고 두번째 파라미터는 분수로, 각 기간이 얼마나 많은 초로 이루어져 있는지를 표현하는 수치이죠. 예컨대, 몇 분(minute) 단위를 short자료형으로 저장하려 한다면, 이 duration 형은 std::chrono::duration<short, std::ratio<60,1>>이 됩니다. 1분에는 60초가 있기 때문이죠. 반대로, 밀리초(millisecond)단위를 double에 저장하려 한다면, std::chrono::duration<double, std::ratio<1,1000>> 이 됩니다. 1초 = 1000 밀리초 니까요.

표준 라이브러리는 std::chrono 네임스페이스에 몇가지 미리 정의된 duration들을 제공합니다. 나노초, 마이크로초, 밀리초, 초, 분, 시 들이죠.(nanoseconds, microseconds, milliseconds, seconds, minutes, and hours) 이 기간 단위들은 당신이 원하는 표현을 선택하기에 충분히 다양하죠. (가령 500년 이상의 기간을 표현하기에). SI 비율에 대한 정의도 있는데, std::atto에서 std::exa까지 있습니다. (만약 당신의 플랫폼이 128비트 정수형을 쓴다면!) 이걸 사용해서 std::duration<double,std::centi>처럼 자신만의(custom) 기간 단위를 정의할 수 있죠.

기간들 간의 변환은 값의 버림이 요구되지 않는 한(더 적은 '정밀도'로의 narrowing이 아닌 한)묵시적으로 이루어집니다. 때문에 시간에서 초로 변환하는 것은 괜찮지만, 초에서 시간으로 변환하는 것은 묵시적으로 이루어지지 않죠.) 명시적으로 변환하는 것은 std::duration_cast<>를 통해서 할 수 있습니다.

std::chrono::milliseconds ms(54802);
std::chrono::seconds s = std::chrono::duration_cast<std::chrono::seconds> (ms)
// 밀리 초에서 초 단위로의 변환. 정밀도가 감소하기 때문에 명시적 변환이 필요하다.

이 결과값은 반올림되기 보다는 버려집니다(truncation). 그러니 위 예시에서 s는 (반올림 값인 55가 아니라) 54라는 값을 가지게 되죠.

Duration(기간)은 산술연산도 지원합니다. 그러니 당신이 원한다면 duration 간의 덧셈, 뺄셈이나 상수와의 곱셉, 나눗셈이 가능하죠. 그러니 5*seconds(1)seconds(5) 또는 minutes(1) - seconds(55)와 같습니다. duration에서 단위 수(서수)는 count() 멤버 함수를 사용해서 얻을 수 있습니다. 그러니 std::chrono::milliseconds(1234).count()는 1234인 것이죠.

Duration-based(기간을 사용한) 대기는 std::chrono::duration<> 개체(instance)를 필요로 합니다. 예컨대, 당신이 future가 준비되기까지 최대 35 밀리초간 대기한다면, 이런식으로 표현할 수 있습니다.

std::future<int> f = std::async(some_task);
if( f.wait_for( std::chrono::milliseconds(35) ) == std::future_status::ready )
{
   do_something( f.get() );
}

wait 함수는 timeout이 되었는지 대기중인 이벤트가 발생했는지를 반환합니다. 이 경우, 당신은 future를 기다리고 있을 겁니다.
그러니 wait함수는
timeout이 발생해 std::future_status::timeout을 반환하거나,
future변수가 준비되어서 std::future_status::ready를 반환하거나,
future의 task가 deferred되어서 std::future_status::deferred를 반환합니다.
duration-based 대기에서 duration은 라이브러리 내부의 steady_clock을 이용해서 측정됩니다. 그러니 35밀리초는 elapsed time의 35 밀리초를 의미하죠. system clock이 대기시간동안 조정되었더라도 말이죠.

앞서 system_clock은 steady하지 않을 수 있다는 점을 언급했고, 그렇기 때문에 라이브러리 내에 정의된 steady_clock을 통해서만 duration을 계산한다는 의미로 해석됩니다.

물론, 시스템 스케줄링의 변덕성(vagaries)과 운영체제 시계(OS clock)의 정확성에 따라서, thread가 호출/반환을 하는데 걸리는 시간은 35밀리초보다 길어질 수 있습니다.

자, duration을 배웠으니, time point(시점)으로 이동해보죠.

  • Time Points (시점)

시계(clock)을 위한 시점은 std::chrono::time_point<>의 인스턴스로 표현됩니다. 첫번째 템플릿 파라미터는 어떤 시계를 참조할 것인지, 두번째 파라미터는 시간의 측정단위(std::chrono::duration<>의 상세 자료형)로 구성되죠. 시점의 값은 시간의 길이(명시된 duration의 배수)입니다. 시간에서 임의의 지점(point)은 epoch라고 불리기 떄문이죠. clock에서 epoch는 기본속성이지만, 바로 획득되거나 C++ 표준에서 명세하는 것이 아닙니다.(directly available to query or specified by the c++ standard.) 일반적인 epoch는 1970년 1월 1일 00:00과 application이 부팅 될 때의 time_point인스턴스를 포함하죠. Clock은 epoch를 공유하거나, 독립적이니 epoch를 소유할 수 있습니다. 만약 두 clock이 하나의 epoch를 공유한다면, 한 클래스의 time_point typedef를 다른 clock 타입도 그 time_point와 연관되도록 할 수 있습니다. 비록 epoch가 언제인지 알 수 없더라도, 당신은 임의의 시점(time point)에서 time_since_epoch()를 계산해낼 수 있죠. 이 멤버함수는 특정 시점(우리가 알수 없던 그 epoch)으로부터의 시간길이를 duration 값으로 반환합니다.

예컨대, time_pointstd::chrono::time_point<std::chrono::system_clock, std::chrono::minutes>로 정의했다면, 이 time_point에서는 상대 system clock값을 분 단위로 보유하게 됩니다.(system_clock의 기본 정밀도는 초이거나 더 작기 때문에 상대적으로 덜 정밀하게 되죠.)

std::chrono::time_point<> 인스턴스에는 duration 을 더하거나 뺄 수 있습니다. 그러니 std::chrono::high_resolution_clock::now() + std::chrono::nanoseconds(500)은 500 나노초 뒤의 미래시점(time_point)을 반환하겠죠. 이런 특징은 당신이 코드블럭(block of code)의 최대 duration을 알 때, timeout절대값(absolute timeout)을 계산하는데 유용합니다. 하지만 대기 함수(waiting function)을 여러번 호출 하거나, waiting function을 지나치고 진행하는(precede)함수(non-waiting function)가 호출되면 일부 시간을 잡아먹을 순 있습니다.

물론, 하나의 시점에서 다른 시점을 뺄 수도 있습니다. 이 때 결과는 두 시점의 시간 간격을 duration 값으로 반환해주죠. 이 연산은 코드블럭의 시간을 계산하는 데 유용하게 쓰일 수 있습니다. 예를 들어보죠.


auto start = std::chrono::high_resolution_clock::now();
do_something();
auto stop = std::chrono::high_resolution_clock::now();

std::cout << "do_something() took "
   << std::chrono::duration<double, std::chrono::seconds>(stop-start).count() 
   << " seconds" << std::endl;

// Start와 Stop의 시간차를 duration<> 템플릿을 사용해 인스턴스화. 
// double 자료형으로 저장되며, 측정단위는 second(초).

std::chrono::time_point<> 인스턴스의 clock 파라미터는 epoch의 정의 뿐만 아니라 다른 기능도 합니다. absolute timeout을 가지는 wait함수로 time point의 인스턴스를 넘겨주게 되면, 넘겨준 파라미터는 시간을 측정하는데 사용되죠. 이러한 특징은 시간이 바뀔때 중요한 결과로 이어집니다. wait함수는 이 시간 변화를 추적해서 now()함수가 넘겨받은 timeout 시점 보다 늦지 않는한 반환하지 않기 때문이죠. 만약 그 clock이 앞서 조정되었다면, (파라미터로 전달하기 전에 조정되었다면) wait 함수의 총 대기 시간(steady_clock으로 측정했을 때)을 단축시킬 수 있습니다. 그리고 만약 wait 함수에 전달된 이후에 조정되었다면, wait 함수의 총 대기시간을 증가시키게 되죠.

(wait 함수에 파라미터로 넘겨준다는 점으로 미루어) 예상하셨겠지만, time point는 wait함수의 _until 파생형과 같이 사용되기도 합니다. 일반적인 사용예시는 프로그램에서 고정된 시점에서부터 some-clock::now()로부의 시간 차(offset)계산입니다. system time과 연동된 time point는 time_t에서 std::chrono::system_clock::to_time_point() 정적 멤버 함수를 호출하는 것으로 획득할 수 있습니다. 이 값은 사용자에게 가시적인 시간(user-visible time)에 연산을 스케줄링하는데 사용되죠. 예를 들자면, 만약 어떤 condition-variable과 관련된 이벤트의 발생까지 최대 500밀리초 동안 대기한다고 하면, 아마 이런 코드를 작성하게 될 겁니다.

// <Listing 4.11>
#include <condition_variable>
#include <mutex>
#include <chrono>
std::condition_variable cv;
bool done;
std::mutex m;
    
bool wait_loop()
{
   auto const timeout = std::chrono::steady_clock::now()
                        + std::chrono::milliseconds(500);
   std::unique_lock<std::mutex> lk(m);
   while(!done)
   {
      if( cv.wait_until( lk, timeout ) == std::cv_status::timeout )
         break;
   }
   return done;
}

제한된 시간동안 condition variable을 기다린다면 이런 방법을 권장합니다. 만약 당신이 wait함수로 전제조건(predicate)를 넘기지 않는다면 말이죠. 이 방법으로, 루틴(loop)의 전체 길이는 제한됩니다. 4.1.1에서 보았듯이, spurious wakeup을 다루기 위해선, predicate없이 condition variable을 쓸때는 loop를 사용해야 합니다. 만약 wait_for() 함수가 루프 안에서 사용된다면, spurious wake가 발생하기 전까지 거의 최대 길이만큼 대기할 수도 있습니다. 그리고 다음 wait time이 시작되죠. 이렇게 되면 몇번이고 대기를 반복하기 때문에 전체 대기시간은 무기한으로 길어집니다.

timeout을 명세하는 기초적인 방법들을 알았으니, 이제 timeout을 사용하는 함수들로 넘어가 볼까요.

timeout을 사용할 수 있는(accept) 함수

timeout을 사용하는 가장 단순한 사용법은, 특정 thread의 프로세싱에 딜레이를 주는 겁니다. 이렇게 하면 그 thread는 다른 thread들로부터 프로세서를 잡아먹지 않게 되죠. 할게 없으니까요. 당신이 'done'플래그를 루프안에서 poll했던 4.1의 예시에서 봤었죠. 이 역할을 하는 함수가 2개 있는데, std::this_thread::sleep_for()std::this_thread::sleep_until()입니다. 이 두 함수는 마치 단순한 알람시계처럼 동작하죠. thread는 임의의 기간(duration)동안 (sleep_for()) 또는 임의의 시점(time point)까지 (sleep_until()) sleep 상태에 빠지게 되죠. 4.1 부분에서의 예시들은 sleep_for()가 적합하죠. 어떤 일이 주기적으로 진행되어야 하거나, 종료시간(elapsed time)을 고려해야하는 경우들이었습니다. 반면에, sleep_until()은 호출하는 thread가 특정한 시점에 일어나도록 조절할 수 있죠. 새벽에 백업을 시작하도록 한다거나, 오전 6시에 payroll을 출력하도록 한다거나, 또는 비디오를 되감을 때 다음 프레임이 갱신되면 스레드를 종료하도록 하는 것이 가능합니다.

물론, timeout의 기능이 sleeping만 있는 것은 아니죠. 이미 봤었지만 future와 condition variable과 timeout을 같이 쓸 수 있습니다. 심지어 뮤텍스로 락을 거는 것도 (뮤텍스가 지원한다면) timeout을 이용해서 할 수 있었죠. 일반적인 std::mutexstd::recursive_mutex는 잠금시에 timeout을 지원하지 않습니다. 하지만 std::timed_mutex는 지원하죠. std::recursice_timed_mutex도 지원합니다. 이 두 타입들은 try_lock_for()try_lock_until() 멤버함수를 지원하고 임의의 기간이나 시점까지 잠금을 얻으려고 하죠. 표 4.1은 C++ 표준 라이브러리에서 timeout을 사용할 수 있는 함수들을 보여줍니다. duration으로 표시된 파라미터들은 std::duration<>의 인스턴스여야만 합니다. 그리고 time_point로 표시된 파라미터들은 std::time_point<>의 인스턴스여야 하죠.

[표4.1]

스크린 샷 형태로 추가 예정

이제까지 condition variable, future, promises, 그리고 packaged task에 대해서 다루었습니다. 이 기능들을 사용해서 어떻게 thread들 간의 연산을 동기화할 수 있는지 큰 그림을 그려보죠.

4. 코드 단순화를 위한 연산의 동기화

Using synchronization of operations to simplify code

이번 단원에서 그동안 설명했던 동기화 기능들을 사용하는 것은 당신이 동기화가 필요한 연산에 집중할 수 있도록 해줍니다. 코드 단순화를 도울 수 있는 방법 중 하나는, 프로그램의 병렬성에 대해서 코드를 좀 더 함수적(funtional)으로 작성하는(accomodates) 접근법을 취하는 것입니다. Thread간의 데이터를 공유하는 것 보다, 각 연산(task)들에게 필요한 정보가 주어지고, 결과가 다른 thread들에게 future를 사용해서 전파(disseminate)되는 것이죠.

future를 사용한 함수형 프로그래밍

함수형 프로그래밍(FP : Functional Programming)이라는 용어는 함수의 결과가 오직 그 함수에게 전달되는 매개변수(parameter)에 의해서만 결정되고, 함수 외부의 상태와는 분리되어 있는(의존하지 않는) 프로그래밍 스타일을 의미합니다. 이런 방법은 함수의 수학적 개념과 관련되어 있습니다. 그리고 이것의 의미는, 함수를 같은 인자(parameter)로 2번 호출하면, 그 결과는 완전히 똑같다는 것이죠. 이것이 C++ 표준 라이브러리에 있는 sin, cos, 그리고 sqrt와 같은 수학적인 함수들의 속성(property)입니다. 단순한 타입들의 기본연산도 마찬가지 입니다. 3+3, 6*9, 1.3/4.7처럼요. 순수한(pure) 함수의 효과는 온전히 반환 값으로만 제한됩니다.

Side effect를 최소화하는 함수들로 프로그램을 구성해야 한다는 이야기.

이렇게 되면 생각하기 쉬워집니다. 특히 동시성(concurrency)이 개입할때 그렇죠. 왜냐하면 공유 메모리와 관련된 (3장에서 다루었던) 대부분의 문제들이 사라지니까요. 공유 데이터에 변경이 없다면, 경쟁 상태(race condition)가 사라지고, 따라서 공유 데이터를 mutex를 사용해서 보호할 필요도 없어집니다. 마치 병렬적인 시스템에서 프로그래밍을 하는데 점차 더 사용되고 있는 Haskell 프로그래밍 언어처럼 강력한 단순화를 보여주죠. Haskell에서 모든 함수들은 기본적으로(by default) 순수(pure)합니다. 대부분이 순수하기 때문에, 순수하지 않은(impure) 함수들은 실제로 공유 상태를 변경합니다. 그러니 어떻게 이 순수하지 않은 함수들이 어플리케이션(application)의 전체구조에 적합하게 사용될지 고려하기(reason) 쉬워지는 것이죠. 함수형 프로그래밍의 이점들은 함수형 프로그래밍 패러다임에만 국한되지는 않습니다. 무엇보다(however), C++는 멀티 패러다임 언어이고, 프로그램을 함수형 프로그래밍 스타일로 작성하는 것이 가능합니다. C++11은 C++98에 비해 이 부분에서도 쉽죠.
람다(lambda) 함수(Ref : appendix A, section A.7),
Boost와 TR1의 std::bind와의 병합(incorporation),
auto 자동 타입 추론기능(Ref : appendix A, section A.7)가 있으니까요.
그리고 future는 C++에서 FP 스타일 동시성을 구현하는 마지막 요소 입니다. future는 thread들간에 전달될 수 있고, 한 계산의 결과를 다른 계산의 결과에 의존하도록 만들 수 있습니다. 공유 데이터에 대한 명시적 접근 없이 말이죠.

FP-스타일 Quick-Sort

FP-스타일 동시성을 위해서 future를 사용하는 것을 보여드리죠. 단순한 Quick-Sort 알고리즘을 통해서 확인해봅시다. 이 알고리즘의 기본 아이디어는 단순한데, 값들의 리스트(선형 집합)가 주어지면, 리스트의 한 원소 기준점(pivot)으로 삼고, 리스트를 기준점보다 작은 부분(partition)과 크거나 같은 부분으로 나눕니다. A sorted copy of the list is obtained by sorting the two sets and returning the sorted list of values less than the pivot, followed by the pivot, followed by the sorted list of values greater than or equal to the pivot. 그림 4.2는 10개의 정수가 이 방법으로 정렬되는 것을 보여줍니다. FP-스타일의 절차적인 구현형태는 아래쪽에 있습니다. 이 구현은 std::sort()처럼 배치상태를 바꾸기 보다는 리스트를 값으로 반환(return by value) 합니다.

// [Listing 4.12 : A sequental implementation of Quicksort]

template <typename T>
std::list<T> sequential_quick_sort(std::list<T> input)
{
     if( input.empty() ){
          return input;
     }

     std::list<T> result;    

     result.splice( result.begin(), input, input.begin() );     // 1.

     T const& pivot = *result.begin();          // 2.

     auto divide_point = std::partition( input.begin(), input.end() ,
                    [&](T const& t) {return t<pivot;}  
               );     // 3. 

     std::list<T> lower_part;
     lower_part.splice( lower_part.end(), input, input.begin(), divide_point );     // 4.

     auto new_lower( 
               sequential_quick_sort( std::move(lower_part) )  );     // 5.
     auto new_higher(
               sequential_quick_sort( std::move(input) )  );     // 6. 

     result.splice(result.end(), new_higher);     // 7.
     result.splice(result.begin(), new_lower);     // 8.
     
     return result;
}

비록 FP-스타일이긴 하지만, 이대로라면 많은 복사가 발생하게 됩니다. 그렇기 때문에 내부적으로는 '일반적인' 스타일을 사용할 필요가 있죠.
\1. 당신은 splice()를 사용해서 리스트를 잘라내면서 첫번째 원소를 기준점(pivot)으로 지정합니다.
\2. 이 방법은 잠재적으로 차선의(suboptimal) 정렬(비교나 교환횟수의 측면에서)로 이어집니다만,std::list로 무엇인가 하는 것은 리스트 순회로 인해서 시간을 더 소비할 여지가 있습니다.
\3. 다음에는 std::partition을 사용해서 리스트를 pivot보다 작은 값들과 작지 않은 값들로 분할(divide)합니다.

분할 기준을 명시하는 가장 쉬운방법은 람다(lambda) 함수를 사용하는 것입니다. 참조 캡쳐(reference capture)를 사용해서 pivot을 값으로 복사하는것을 방지해야 하죠. (Ref : appendix A, section A.7)

\4. 이제, FP스타일 형식(interface)으로 최적화되었습니다. 그러니 만약 당신이 절반으로 나누어 재귀를 사용하면, 2개의 리스트들이 필요하죠. 이 부분은 splice()를 사용해, input에서 divide_point까지 잘라내어 lower_part리스트로 집어넣으면 됩니다.
\5.\6. 이렇게 하면 남아있는 값들은 input에 남아있게 되죠. 이 두 리스트(inputlower_part)를 재귀호출로 정렬합니다.

\7. 여기서도 std::move를 사용하면 다시 복사를 막을 수 있죠. 결과값은 묵시적으로 옮겨집니다. 마지막으로, splice()를 다시 쓰면 result를 올바른 순서로 맞추게 됩니다(piece). new_higher 값은 마지막에 위치하게 되죠.
\8. new_higher값은 pivot 뒤에 위치하며, new_lower값은 시작부분, pivot앞쪽에 위치합니다.

FP-스타일 병렬(parallel) Quick-Sort

On-work

메세지 전달을 통해서 연산 동기화하기

On-work

5. 요약(Summary)

스레드 간 연산을 동기화 하는 건 동시성을 사용하는 프로그램을 작성하는데 중요한 요소입니다. 만약 동기화가 없다면, 그 스레드들은 필수적으로 독립적이고 분리된 어플리케이션들로 작성되어야 할 겁니다. 서로 연관된 일을 처리하는 한 그룹 처럼 움직이도록 말이죠. 이번 단원에서는, 연산들을 동기화하는 다양한 방법을 다뤘습니다. 간단한 조건 변수, future 와 promise, 그리고 Packaged tasks가 있었죠. 동기화 문제에 어떻게 접근할 것인가도 다루었습니다. 입력에 따라서만 결과가 나오는 함수형 프로그래밍이나, 스레드간 통신이 비동기 메세징으로 이루어지는 메세지 전달이 있었죠.

많은 C++에서 가능한 고-레벨 기능들을 다뤘지만, 이제 이 모든것들이 가능하게 해주는 깊은-레벨 기능들에 대해서 다룰 차례입니다. 바로 C++ 메모리 모델과 atomic 연산들입니다.