Upload
jintaek-seo
View
569
Download
0
Embed Size (px)
Citation preview
멀티쓰레드 프로그래밍 :01. 쓰레딩 기본
2014 년 10 월 8일서진택 , [email protected]
목차 index문장 statement 과 표현식 expression rvalue reference Move semantics쓰레드 thread
2
문장 statement 과 표현식 expression
문장 statement 과 표현식 expressionrvalue referenceMove semantics쓰레드 thread
3
문장 statement 과 표현식 expression Statement 를 구성하는 일부로서 , 값 value 을 가지면 expression 이라고 합니다 .
Statement 전체가 값 value 을 가진다면 , 그 statement 는 expression 입니다 .
expression 은 항상 statement 입니다 .– i = i + 4;– 위 문장에서 expression 은 다음과 같은 것들입니다 .– i– i+4– i=i+4
4
아래의 소스에서 expression 인 문장은 2 개입니다 .int GetSum( int iLeft_, int iRight_ ){ return iLeft_ + iRight_;}//GetSum()
void Test( const int& iData_ ){ std::cout << __FUNCTION__ << " const reference" << std::endl;}//Test()
int main(){ int i = 3; int j = 0; j = GetSum( 4, 5 ); // 문장이 값 9 를 가지므로 expression 입니다 . if( j == 9 ) { printf( "%i\r\n", j ); // 문장이 값 3 을 가지므로 expression 입니다 . }//if Test( i );}5
표현식의 lvalue/rvalueint main(){ int i = 3; int j = 0; i = i + 4; //(1) Test( i );
6
i 의 주소가 [5000] 번지라고 가정합니다 . 32bit 환경에서 j의 주소는 [4996] 번지가 될 것입니다 .같은 변수 i에 대해서 i가 등호 =의 왼쪽에 사용되면 [5000] 으로 해석하고 , i 가 등호의 오른쪽에 사용되면 [5000] 번지가 가리키는 값 , 즉 3 으로 해석한다는 것에 주목하세요 .즉 변수 i는 사용되는 환경에 따라 expression 에서 평가되는 값 value 이 달라집니다 .표현식은 값을 가지는데 , 표현식이 등호 =의 왼쪽에 올 때 가져야 하는 값을 lvalue, 등호의 오른쪽에 올 때 가져야 하는 값을 rvalue 라고 합니다 .명심하세요 . lvalue/rvalue 는 문장이나 변수에는 해당하지 않습니다 . lvalue/rvalue 는 표현식expression 이 가지는 값입니다 .(1)문장에서 등호의 왼쪽에 있는 표현식 i는 lvalue 를 가지며 , lvalue 의 값은 [5000] 입니다 . (1) 문장에서 등호의 오른쪽에 있는 표현식 i+4 는 rvalue 를 가지며 , rvalue 의 값은 7입니다 .i=i 라는 문장에서 등호의 왼쪽에 있는 표현식 i 는 lvalue 를 가지며 , 값은 [5000] 입니다 . 등호의 오른쪽에 있는 i 는 rvalue 를 가지며 값은 3 입니다 .
rvalue 를 함수의 인자로 전달void Test2( int i, int j ){ printf( "%p, %p\r\n", &i, &j );}//Test2()
int main(){ int i = 3; int j = 0; Test2( 5, i );}//main()
7
표현식의 rvalue 가 값을 인자로 받는 함수로 전달하는 경우 , 호출된 함수 내부에서는 rvalue 성질이 유지되지 않습니다 .main() 에서 Test2() 를 호출할 때 , rvalue 5 를 넘겼지만 , Test2() 함수 내부에서는 이 값을 이름name i 로 참조할 수 있기 때문입니다 .
lvalue referencevoid Test2( int i, int& j ){ printf( "%p, %p\r\n", &i, &j );}//Test2()
int main(){ int i = 3; int j = 0; Test2( i, i );}//main()
8
Test2() 가 두번째 인자를 참조 lvalue reference 로 받으면 , main() 의 Test2() 호출은 같은 표현식 i에 대해서 첫번째 i는 rvalue 3 을 전달하고 , 두번째 i 는 lvalue [5000] 을 전달합니다 .왜냐하면 Test2( …, int& j) 가 두번째 인자 parameter 의 lvalue reference 를 요구하고 있기 때문입니다 .Test2( i, 5 ); 처럼 호출하면 컴파일 시간 에러가 발생합니다 . 왜냐하면 표현식 5의 lvalue 를 구할 수 없기 때문입니다 .5+3 이나 i+5 같은 이름을 가지지 않는 표현식은 rvalue 만 가집니다 .
Test2( i, 5 ); // error
rvalue 도 어딘가에 할당됨void Test2( int i, const int& j ){ printf( "%p, %p\r\n", &i, &j );}//Test2()
int main(){ int i = 3; int j = 0; Test2( i, 5 );}//main()
9
하지만 const lvalue reference 를 받으면 , rvalue 를 가지는 표현식을 함수의 인자로 전달하는 것이 가능합니다이것은 컴파일러가 생성한 코드가 함수로 전달하는 표현식의 값을 , 사용자가 접근할 수 없는 어떤 메모리 공간에 저장하기 때문에 가능한 일입니다 .프로그래머는 정상적인 방법으로 [4992] 를 알아낼 방법이 없습니다 .j == 5, &j==[4984] 이지만 , *j는 에러입니다 .컴파일러가 이미 rvalue 의 주소값을 전달하고 있다면 프로그래머가 의도적으로 rvalue 의 주소값을 지속적으로 전달하도록 할 수 없을까요 ? 그것이 rvalue reference 입니다 .
rvalue reference, std::move
문장 statement 과 표현식 expressionrvalue referenceMove semantics쓰레드 thread
10
rvalue reference함수가 rvalue reference 를 받도록 인자를 선언할 수 있습니다 .
타입 이름과 변수 이름 사이에 && 를 사용합니다 .– int&& iData_
그러면 컴파일러는 인자에 해당하는 표현식이 rvalue 를 가지는 경우 , rvalue 의 주소를 전달하도록 코드를 생성합니다 .
11
rvalue referencevoid Test( int& iData_ ){ std::cout << __FUNCTION__ << " reference" << std::endl;}//Hello()
void Test( const int& iData_ ){ std::cout << __FUNCTION__ << " const reference" << std::endl;}//Test()
void Test( int&& iData_ ){ std::cout << __FUNCTION__ << " rvalue reference" << std::endl;}//Test()
12
int main(){ int i = 3; int j = 0; Test( 5 ); Test( j == 5 ); Test( j );
/** output: Test rvalue reference Test rvalue reference Test reference 계속하려면 아무 키나 누르십시오 . . . */
rvalue 인자를 다시 rvalue 인자로 전달하기void Hello( int& iData_ ){ std::cout << "Hello reference" << std::endl;}//Hello()
void Hello( int&& iData_ ){ std::cout << "Hello rvalue reference" << std::endl;}//Hello()
void Test( int&& iData_ ){ std::cout << __FUNCTION__ << " rvalue reference" << std::endl; Hello( iData_ );}//Test()
13
Test() 는 rvalue reference iData_ 를 받았지만 , Test() 내부에서 iData_ 는 이제 이름으로 참조할 수 있으므로 (스택에 할당되고 변수의 주소가 &iData_ 이므로 ), iData_ 라는 변수 표현식은 더 이상 rvalue 가 아닙니다 .그러므로 Test() 내부의 Hello( iData ); 호출은 lvalue reference 를 인자로 받는 Hello(int& iData_) 를 호출합니다 .
void Hello( int&& iData_ ){ std::cout << "Hello rvalue reference" << std::endl;}//Hello()
template<typename T>typename std::remove_reference<T>::type&& MyMove( T&& t_ ){ return static_cast<std::remove_reference<T>::type&&>(t_);}//MyMove()
void Test( int&& iData_ ){ std::cout << __FUNCTION__ << " rvalue reference" << std::endl; Hello( MyMove( iData_ ) );}//Test()
14
rvalue reference 를 받은 함수가 내부에서 호출하는 다른 함수에 인자의 rvalue reference 를 그대로 전달하는 방법은 컴파일러로 하여금 강제로 캐스팅 casting 하도록 하는 것입니다 .MyMove() 는 인자로 받은 rvalue reference 의 명시적인 rvalue reference 를 리턴합니다 .그러므로 오버로드된 두 개의 Hello() 중에서 Hello(int&&) 를 호출하는 것이 가능합니다 .
std::movevoid Hello( int& iData_ ){ std::cout << "Hello reference" << std::endl;}//Hello()
void Hello( int&& iData_ ){ std::cout << "Hello rvalue reference" << std::endl;}//Hello()
void Test( int&& iData_ ){ std::cout << __FUNCTION__ << " rvalue reference" << std::endl; Hello( std::move( iData_ ) );}//Test()
15
인자로 받은 rvalue reference 의 값을 그대로 유지하는 표준 구현이 std::move() 입니다 .이렇게 외워 두세요 .
인자로 받은 rvalue reference 를 , 다른 함수에 rvalue reference 로 전달하기 위해서는 std::move() 를 반드시 사용해야 합니다 .
Move Semantics
문장 statement 과 표현식 expressionrvalue referenceMove semantics쓰레드 thread
16
Move Semantics deep copy 가 필요한 클래스는 copy constructor 와
copy assignment operator 를 제공해야 합니다 . container 가 deep copy 가 필요한 객체들을 노드 node
로 유지할 때 , 노드의 삽입이 일어날 때 , 임시 객체temporary object 에 대한 copy constructor 와 copy assignment operator 가 호출됩니다 .
임시 객체는 이름이 없으므로 rvalue 입니다 . 클래스 생성자가 이러한 rvalue 에 대해서 특별하게 동작하
도록 만든다면 , 임시 객체를 위한 복사 동작을 향상 할 수 있습니다 .
이것을 이동 문맥 move semantics 이라고 합니다 . move semantics 는 move constructor 와 move
assignment operator 를 통해 구현합니다 .17
class MemoryBlockclass MemoryBlock{public:
// Simple constructor that initializes the resource. explicit MemoryBlock(size_t length);
// Destructor. ~MemoryBlock();
// Copy constructor. MemoryBlock(const MemoryBlock& other);
// Copy assignment operator. MemoryBlock& operator=(const MemoryBlock& other);
// Retrieves the length of the data resource. size_t Length() const;
private: size_t _length; // The length of the resource. int* _data; // The resource.};
18
MemoryBlock. 생성자와 파괴자// Simple constructor that initializes the resource. explicit MemoryBlock(size_t length) : _length(length) , _data(new int[length]) { std::cout << "In MemoryBlock(size_t). length = " << _length << "." << std::endl; } // Destructor. ~MemoryBlock() { std::cout << "In ~MemoryBlock(). length = " << _length << "."; if (_data != NULL) { std::cout << " Deleting resource."; // Delete the resource. delete[] _data; } std::cout << std::endl; }
19
정수를 MemoryBlock 에 할당할 때 , 생성자가 호출되는 것을 방지하기 위해 , explicit 이 필요합니다 .
MemoryBlock. 복사 생성자와 할당연산자 // Copy constructor. MemoryBlock(const MemoryBlock& other) : _length(other._length) , _data(new int[other._length]) { std::cout << "In MemoryBlock(const MemoryBlock&). length = " << other._length << ". Copying resource." << std::endl;
std::copy(other._data, other._data + _length, _data); }
// Copy assignment operator. MemoryBlock& operator=(const MemoryBlock& other) { std::cout << "In operator=(const MemoryBlock&). length = " << other._length << ". Copying resource." << std::endl;
if (this != &other) { // Free the existing resource. delete[] _data;
_length = other._length; _data = new int[_length]; std::copy(other._data, other._data + _length, _data); } return *this; }
20
MemoryBlock.Move constructor // Move constructor. MemoryBlock(MemoryBlock&& other) : _data(NULL) , _length(0) { std::cout << "In MemoryBlock(MemoryBlock&&). length = " << other._length << ". Moving resource." << std::endl;
//*this = std::move( other ); // Copy the data pointer and its length from the // source object. _data = other._data; _length = other._length;
// Release the data pointer from the source object so that // the destructor does not free the memory multiple times. other._data = NULL; other._length = 0; }
21
MemoryBlock.Move assignment operator // Move assignment operator. MemoryBlock& operator=(MemoryBlock&& other) { std::cout << "In operator=(MemoryBlock&&). length = " << other._length << "." << std::endl;
if (this != &other) { // Free the existing resource. if( _data != nullptr ) delete[] _data;
// Copy the data pointer and its length from the // source object. _data = other._data; _length = other._length;
// Release the data pointer from the source object so that // the destructor does not free the memory multiple times. other._data = NULL; other._length = 0; } return *this; }
22
MemoryBlock.Move constructor.cont // Move constructor. MemoryBlock(MemoryBlock&& other) : _data(NULL) , _length(0) { std::cout << "In MemoryBlock(MemoryBlock&&). length = " << other._length << ". Moving resource." << std::endl;
*this = std::move( other ); }
23
MemoryBlock.main()int main(){ // Create a vector object and add a few elements to it. std::vector<MemoryBlock> v; v.push_back(MemoryBlock(25)); v.push_back(MemoryBlock(75));
// Insert a new element into the second position of the vector. v.insert(v.begin() + 1, MemoryBlock(50));}
24
In Vs2010 or aboveIn MemoryBlock(size_t). length = 25.In MemoryBlock(MemoryBlock&&). length = 25. Moving resource.In ~MemoryBlock(). length = 0.In MemoryBlock(size_t). length = 75.In MemoryBlock(MemoryBlock&&). length = 25. Moving resource.In ~MemoryBlock(). length = 0.In MemoryBlock(MemoryBlock&&). length = 75. Moving resource.In ~MemoryBlock(). length = 0.In MemoryBlock(size_t). length = 50.In MemoryBlock(MemoryBlock&&). length = 50. Moving resource.In MemoryBlock(MemoryBlock&&). length = 50. Moving resource.In operator=(MemoryBlock&&). length = 75.In operator=(MemoryBlock&&). length = 50.In ~MemoryBlock(). length = 0.In ~MemoryBlock(). length = 0.In ~MemoryBlock(). length = 25. Deleting resource.In ~MemoryBlock(). length = 50. Deleting resource.In ~MemoryBlock(). length = 75. Deleting resource.
25
Before Vs2010In MemoryBlock(size_t). length = 25.In MemoryBlock(const MemoryBlock&). length = 25. Copying resource.In ~MemoryBlock(). length = 25. Deleting resource.In MemoryBlock(size_t). length = 75.In MemoryBlock(const MemoryBlock&). length = 25. Copying resource.In ~MemoryBlock(). length = 25. Deleting resource.In MemoryBlock(const MemoryBlock&). length = 75. Copying resource.In ~MemoryBlock(). length = 75. Deleting resource.In MemoryBlock(size_t). length = 50.In MemoryBlock(const MemoryBlock&). length = 50. Copying resource.In MemoryBlock(const MemoryBlock&). length = 50. Copying resource.In operator=(const MemoryBlock&). length = 75. Copying resource.In operator=(const MemoryBlock&). length = 50. Copying resource.In ~MemoryBlock(). length = 50. Deleting resource.In ~MemoryBlock(). length = 50. Deleting resource.In ~MemoryBlock(). length = 25. Deleting resource.In ~MemoryBlock(). length = 50. Deleting resource.In ~MemoryBlock(). length = 75. Deleting resource.
26
쓰레드 Thread
문장 statement 과 표현식 expressionrvalue referenceMove semantics쓰레드 thread
27
쓰레드 thread프로세스 내에서 실행되는 흐름의 단위입니다 .윈도우즈에서 실행되는 온라인게임의 경우 대부분 WinMain() 에서 하나의 쓰레드가 실행됩니다 .
필요에 따라 다른 쓰레드를 만들고 실행할 수 있습니다 .
프로세스가 2개 이상의 쓰레드를 실행하면 멀티쓰레드 Multithread 프로그램입니다 .
Critical Section Mutex Semaphore TLS(Thread Local Storage)
28
쓰레드 구현 Win32 구현이 CreateThread() 입니다 . Microsoft 구현이 _beginthreadex() 입니다 .표준 라이브러리 구현이 std::thread 입니다 . boost 구현이 boost::thread 입니다 .
29
임계영역 Critical Section
두 개의 쓰레드가 같은 루틴을 실행할 수 있습니다 . 그 루틴이 동시에 실행되어서는 안 되는 코드블록이면 그것을 Critical
Section 이라고 합니다 . Critical Section 의 진입과 탈출을 제어하는 객체를 Mutex 라고 합니다
.30
Mutex Mutual Exclusion 의 약자입니다 .일반적으로 운영체제가 제공하는 동기화 객체입니다 . lock() 과 unlock() 을 제공하며 , lock() 과 unlock() 사이의 코드 블록이 Critical Section 입니다 .
lock() 을 시도한 쓰레드가 unlock() 해야 합니다 .
31
Mutex Mutex 의 Win32 구현이 CRITICAL_SECTION 입니다 .
– EnterCriticalSection()– LeaveCriticalSection()– InitializeCriticalSection()– DeleteCriticalSection()
Mutex 의 표준 구현이 std::mutex 입니다 .– std::mutex 의 RAII 헬퍼가 std::lock_guard 입니다 .
Mutex 의 boost 구현이 boost::mutex 입니다 .
32
세마포 Semaphore
B 쓰레드가 A쓰레드의 작업 완료를 기다려야 하는 상황이 있습니다 . 이렇게 여러 쓰레드 사이의 동기화를 제공하는 객체를 Semaphore 라고 합
니다 . 일반적으로 Signal() 과 Wait() 인터페이스를 제공합니다 .
33
세마포세마포의 Win32 구현이 Event 입니다 .
– CreateEvent()– CloseEvent()– SetEvent()– ResetEvent()– WiatForSingleObject()
세마포의 표준 라이브러리 구현이 std::condition_variable 입니다 .
34
데드락 Deadlock– 어떤 쓰레드도 Critical Section 에 진입하지 못하는 상황입니다 .– Crash 처럼 프로그램이 종료하지 않습니다 .– 하지만 아무것도 할 수 없습니다 .
레이스 조건 Race Condition– 쓰레드가 Critical Section 에 서로 진입하려는 상황입니다 .
굶어죽음 Starvation– Race 상황에서 특정 쓰레드만 Critical Section 에 진입하지 못하는
상황입니다 .
35
쓰레드 모델각 쓰레드가 자신이 맡은 고유의 작업을 수행합니다.– 대부분의 게임 엔진이 이 모델을 사용합니다 .– 쓰레드들은 메시지 큐를 통하여 통신합니다 .
임의의 n개 쓰레드는 다른 작업 Task 를 수행합니다.– Task Parallel 하다고 합니다 .– 이러한 모델은 스케일러블 Scalable 합니다 .– 즉 Core 의 개수가 늘어나면 쓰레드의 개수를 늘리면 됩니다 .
임의의 n개 쓰레드는 데이터 영역이 다른 같은 작업을 수행합니다 .– Data Parallel 하다고 합니다 .– 이러한 모델 역시 Scalable 합니다 .
최근의 쓰레드 라이브러리들은 Data Parallelism 과 Task Parallelism 모두 지원합니다 .
36
TLS,Thread Local Storage
어떤 함수가 같은 변수를 접근하는 것 처럼 보이지만 , 쓰레드마다 유일한 자신의 변수를 접근하도록 변수를 선언할 수 있습니다 .
전역변수처럼 선언하지만 , 쓰레드마다 구별되는 변수입니다 . 이러한 변수를 TLS 라고 합니다 . TLS 의 Win32 구현이 TlsAlloc()류의 함수들입니다 . TLS 의 Microsoft 구현이 __declspec( thread ) 입니다 . TLS 의 boost 구현이 boost::thread_specific_ptr<>입니다 .
37
참고 자료 Threading
– http://en.wikibooks.org/wiki/C%2B%2B_Programming/Threading
Thread Support Library– http://en.cppreference.com/w/cpp/thread
Mutex 와 Semaphore 의 차이– http://stackoverflow.com/questions/62814/difference-between-
binary-semaphore-and-mutex
std::condition_variable– http://en.cppreference.com/w/cpp/thread/condition_variable
38
끝
39
멀티쓰레드 프로그래밍 :02. 쓰레드 라이브러리
2014 년 10 월 20일[email protected]
목차 index std::thread std::mutex std::unique_lock std::condition_variable TLS atomic operations memory barriers
41
std::thread
42
#include <thread>#include <iostream>
void my_thread_func(){ std::cout<<"hello"<<std::endl;}
int main(){ std::thread t(my_thread_func); t.join();}
43
std::thread 는 RAII 형식으로만 thread callback 을 실행할 수 있습니다 .join() 은 쓰레드 t가 종료하기를 기다립니다 .
class bar {public: void foo() { std::cout << "hello from member function" << std::endl; }};
int main(){ bar b; std::thread t(&bar::foo, &b); t.join();}
44
객체의 멤버 함수를 thread callback 으로 전달 할 수 있습니다 .
thread.function object 사용하기#include <thread>#include <iostream>
class SayHello{public: void operator()() const { std::cout<<"hello"<<std::endl; }};
int main(){ std::thread t(SayHello()); t.join();}
45
thread.std::bind 로 함수 객체 만들기#include <thread>#include <iostream>#include <string>#include <functional>
void greeting(std::string const& message){ std::cout<<message<<std::endl;}
int main(){ std::thread t(std::bind(greeting,"hi!")); t.join();}
46
std::bind 를 이용하여 함수 객체를 리턴하도록 합니다 .
thread. 쓰레드 함수로 인자 전달하기#include <thread>#include <iostream>
void write_sum(int x,int y){ std::cout<<x<<" + "<<y<<" = "<<(x+y)<<std::endl;}
int main(){ std::thread t(write_sum,123,456); t.join();}
47
std::thread 의 생성자는 가변 인자를 받도록 설계되어 있습니다 . 생성자 구현이 첫번째 파라미터를 쓰레드 함수로 인식하고 , 나머지 값들을 쓰레드 함수의 인자로 가집니다 .
#include <thread>#include <iostream>
class SayHello{public: void greeting(std::string const& message) const { std::cout<<message<<std::endl; }};
int main(){ SayHello x; std::thread t(&SayHello::greeting,&x,"goodbye"); t.join();}
48
thread.shared_ptr 사용하기#include <>
int main(){ std::shared_ptr<SayHello> p(new SayHello); std::thread t(&SayHello::greeting,p,"goodbye"); t.join();}
49
스마트 포인터를 전달하는 것 가능합니다 . 쓰레드 객체 t가 살아 있을 동안 , p 의 lifetime 또한 유지됩니다 .
thread.reference 전달하기#include <thread>#include <iostream>#include <functional> // for std::ref
class PrintThis{public: void operator()() const { std::cout<<"this="<<this<<std::endl; }};
int main(){ PrintThis x; x(); std::thread t(std::ref(x)); t.join(); std::thread t2(x); t2.join();}
50
this=0x7fffb08bf7efthis=0x7fffb08bf7efthis=0x42674098
variadic templateint func() {} // termination version template<typename Arg1, typename... Args>int func(const Arg1& arg1, const Args&... args){ process( arg1 ); func(args...); // note: arg1 does not appear here!}
51
variadic template.specializationtemplate<typename T>class Template{public: void SampleFunction(T param){
}};
template<>class Template<int>{public: void SampleFunction(int param){
}};
52
template<typename... Arguments>class VariadicTemplate{public: void SampleFunction(Arguments... params){
}};
template<>class VariadicTemplate<double, int, long>{public: void SampleFunction(double param1, int param2, long param3){
}};
thread.constructorthread();(1)(since C++11)thread( thread&& other ); (2)(since C++11)template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args ); (3)(since C++11)thread(const thread&) = delete ;(4)(since C++11)Constructs new thread object.
53
표준 constructor 는 variadic template 으로 선언되어 있지만 , Vs2012 의 실제 구현은 BOOST_PP 와 비슷한 구현의 가변 매크로를 사용하여 구현되어 있습니다 .
std::mutex
54
thread.mutexstd::mutex m;std::string s;void append_with_lock_guard(std::string const& extra){ std::lock_guard<std::mutex> lk(m); s+=extra;}void append_with_manual_lock(std::string const& extra){ m.lock(); try { s+=extra; m.unlock(); } catch(...) { m.unlock(); throw; }}55
std::lock_guard 를 이용하여 RAII 형식으로 예외에도 안전하게 동작하도록 합니다 .
std::unique_lock
56
std::unique_lockstd::mutex mtx; // mutex for critical section
void print_block (int n, char c) { // critical section (exclusive access to std::cout signaled by lifetime of lck): std::unique_lock<std::mutex> lck (mtx); for (int i=0; i<n; ++i) { std::cout << c; } std::cout << '\n';}
int main (){ std::thread th1 (print_block,50,'*'); std::thread th2 (print_block,50,'$');
th1.join(); th2.join();
return 0;}
57
lock_guard 와 같은 의도로 사용할 수 있습니다 .출력결과 :
**************************************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
std::unique_lock<std::mutex> acquire_lock(){ static std::mutex m; return std::unique_lock<std::mutex>(m);}
//std::mutex mtx; // mutex for critical section
void print_block (int n, char c) { // critical section (exclusive access to std::cout signaled by lifetime of lck): //std::lock_guard<std::mutex> lck (mtx); std::unique_lock<std::mutex> lck = acquire_lock(); for (int i=0; i<n; ++i) { std::cout << c; std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) ); } std::cout << '\n';}
58
move semantics 에 대해 안전하게 동작합니다 . acquire_lock() 이 unique_lock 을 사용하지 않고 lock_guard 를 사용하면 컴파일 타임 에러가 발생합니다 .
rvalue 객체 생성 막기class KPreventRValueObject{private: KPreventRValueObject( KPreventRValueObject&& rvalueref_ );public: KPreventRValueObject(){}
private: std::string m_strData;};//class KPreventRValueObject
KPreventRValueObject TestRValueObject(){ return KPreventRValueObject(); // compile time error}//TestRValueObject()
59
move semantic 구현하기class data_to_protect{public: void some_operation(){} void other_operation(){}};//class data_to_protect
class data_handle{private: data_to_protect* ptr; std::unique_lock<std::mutex> lk;
friend data_handle lock_data();
data_handle(data_to_protect* ptr_, std::unique_lock<std::mutex>&& lk_) : ptr(ptr_) , lk( std::move( lk_ ) ) {}
60
public: data_handle(data_handle&& other) : ptr(nullptr) { *this = std::move( other ); } data_handle& operator=(data_handle&& other) { if( &other != this ) { ptr = other.ptr; lk = std::move(other.lk); other.ptr = 0; }//if return *this; } void do_op() { ptr->some_operation(); } void do_other_op() { ptr->other_operation(); }};//class data_handle
61
data_handle lock_data(){ static std::mutex m; static data_to_protect the_data; std::unique_lock<std::mutex> lk(m); return data_handle(&the_data, std::move(lk) );}//lock_data()
int main(){ data_handle dh = lock_data(); // lock acquired dh.do_op(); // lock still held dh.do_other_op(); // lock still held data_handle dh2 = std::move(dh); // transfer lock to other handle dh2.do_op(); // lock still held return 0;}//main()
std::unique_lock : 잠시 unlock 하기std::mutex m;std::vector<std::string> strings_to_process;
void update_strings(){ std::unique_lock<std::mutex> lk(m); if(strings_to_process.empty()) { lk.unlock(); std::vector<std::string> local_strings=load_strings(); lk.lock(); strings_to_process.insert(strings_to_process.end(), local_strings.begin(),local_strings.end()); }}
62
unique_lock 은 RAII 패턴을 사용하면서도 , 원하는 때에 lock/unlock 이 가능합니다 .
deadlock 상황의 예class account{ std::mutex m; currency_value balance;public:
friend void transfer(account& from,account& to, currency_value amount) { std::lock_guard<std::mutex> lock_from(from.m); std::lock_guard<std::mutex> lock_to(to.m); from.balance -= amount; to.balance += amount; }};
63
두개의 쓰레드가 accout.transfer( A, B, … ), account.transfer( B, A, … ) 형태로 호출하면 deaklock이 발생할 수 있습니다 .
struct Box { explicit Box(int num) : num_things{num} {} int num_things; std::mutex m;}; void transfer(Box &from, Box &to, int num){ // don't actually take the locks yet std::unique_lock<std::mutex> lock1(from.m, std::defer_lock); std::unique_lock<std::mutex> lock2(to.m, std::defer_lock); // lock both unique_locks without deadlock std::lock(lock1, lock2); from.num_things -= num; to.num_things += num; // 'from.m' and 'to.m' mutexes unlocked in 'unique_lock' dtors}
64
unique_lock 의 lock 시점을 컨트롤하는 기능을 사용하면 std::lock() 과 사용하여 dead lock 을 예방하는 코드를 작성할 수 있습니다 .
std::condition_variable
65
std::mutex mtx;std::condition_variable cv;bool ready = false;
void print_id (int id) { std::unique_lock<std::mutex> lck(mtx); while (!ready) cv.wait(lck); // ... std::cout << "thread " << id << '\n';}
void go() { std::unique_lock<std::mutex> lck(mtx); ready = true; cv.notify_all();}
66
int main (){ std::thread threads[10]; // spawn 10 threads: for (int i=0; i<10; ++i) threads[i] = std::thread(print_id,i);
std::cout << "10 threads ready to race...\n"; go(); // go!
for (auto& th : threads) th.join();
return 0;}
std::mutex mtx;std::condition_variable cv;bool ready = false;
void print_id (int id) { std::unique_lock<std::mutex> lck(mtx); //while (!ready) cv.wait(lck); cv.wait( lck, []{ return ready;} ); // ... std::cout << "thread " << id << '\n';}
void go() { std::unique_lock<std::mutex> lck(mtx); ready = true; cv.notify_all();}
67
int main (){ std::thread threads[10]; // spawn 10 threads: for (int i=0; i<10; ++i) threads[i] = std::thread(print_id,i);
std::cout << "10 threads ready to race...\n"; go(); // go!
for (auto& th : threads) th.join();
return 0;}
condition_variable.examplestd::mutex g_mutex;std::condition_variable g_conditioVariable;
bool g_bReady = false;bool g_bIsRunThread = true;
void consume (int n){ int iCounter = 0; while( g_bIsRunThread == true ) { { std::unique_lock<std::mutex> ulock( g_mutex ); g_conditioVariable.wait( ulock, []{ return g_bReady;} ); }//block
std::cout << iCounter << std::endl; std::this_thread::sleep_for( std::chrono::milliseconds(500) );
iCounter += 1; }//while}//consume()
68
int _tmain(){ int ich = 0; std::thread consumerThread(consume,10);
std::cout << "Thread started" << std::endl;
while( g_bIsRunThread == true ) { ich = _getch(); if( ich == 'p' ) // pause { std::unique_lock<std::mutex> ulock( g_mutex ); g_bReady = false; } else if( ich == 'c' ) // continue { std::unique_lock<std::mutex> ulock( g_mutex ); g_bReady = true; g_conditioVariable.notify_one(); }
69
else if( ich == 'e' ) // exit { g_bIsRunThread = false;
std::unique_lock<std::mutex> ulock( g_mutex );
g_bReady = true; g_conditioVariable.notify_one(); }//if.. else if.. }//while
consumerThread.join();
return 0;}//main()
TLS
70
Tls 는 하나의 코드 루틴이 서로 다른 쓰레드에서 호출될 때 , 접근하는 전역 메모리를 다르게 설정하는 것이 가능합니다 .
전역 변수를 각 쓰레드별로 나누어서 해당 쓰레드에서 접근하도록 하는 방법과의 차이점은 다음과 같습니다 .– 쓰레드를 위한 전역 변수를 직접 관리하면 각 쓰레드에 dependent 한 코드가 쓰레
드 루틴에 추가되어야 합니다 .– Tls 를 사용하면 코드는 쓰레드와 상관없이 하나의 코드를 사용합니다 .
Boost 는 각 플랫폼에 대한 Tls 구현을 숨긴 ''boost::thread_specifix_ptr<>'' 을 제공합니다 .– 각 쓰레드에서 필요한 메모리인 경우 boost::thread_specifix_ptr<> 타입으로 변수
를 선언합니다 .– 아래 코드에서 문제가 된 부분은 FindNearestSplinePoint() 함수가 단일 쓰레드에
서만 사용하다가 멀티 쓰레드에서 사용하게 되면서 이 함수가 내부에서 사용할 용도로 선언된 static 변수의 read/wirte 동작 때문에 크래시가 발생한 경우 였습니다.
– 그래서 이 함수가 사용하는 static 변수를 thread safe 하게 만들어 주어야 했습니다 .
71
/// TLS(Thread Local Storage) 를 사용하여 각 thread 가 자신의 local memory 를 사용하도록 수정하다 .
/// KpSplineUtil namespace 의 함수들은 thread safe 해야 한다 ./// native type 이 아니므로 ''boost::thread_specific_ptr'' 를 이용한다 ./// - jintaeks on 2013-03-20, 13:35 */static boost::thread_specific_ptr<KpIntervalHeap<KInfo>> s_kIntervalHeap;
bool KpSplineUtil::FindNearestSplinePoint( IN OUT KpSplinePosition& kInOutArgument_ , const KpSpline& kInSpline_ , const KpVector3& vInPoint_ , KpReal rInError_ ){ unsigned uNumSegs = kInSpline_.GetNumSegments(); ASSERT( uNumSegs > 0 ); if( uNumSegs == 0 ) { kInOutArgument_.Invalidate(); return false; }//if
72
KpIntervalInfo<KInfo> kInfo; KpVector3 vMin, vMax;
/// 각 thread 에서 처음으로 호출될 때 , 이 값은 NULL 이다 . /// 그 때 thread 가 사용할 메모리를 할당한다 . /// 프로그램 종료할 때 , 메모리 delete 를 시켜주어야 한다 . /// - jintaeks on 2013-03-20, 13:37 if( s_kIntervalHeap.get() == NULL ) { s_kIntervalHeap.reset( new KpIntervalHeap<KInfo>() ); }//if
s_kIntervalHeap->MakeEmpty();
if( kInOutArgument_.IsValid() ) { ASSERT( kInOutArgument_.m_iIndex < ( int ) uNumSegs );
73
native 타입인 경우는 Visual Studio 2005 부터 지원하는 확장 키워드 __declspec( thread ) 를 사용하여 변수를 선언하면 됩니다 .
코드 상으로 각 쓰레드가 같은 변수를 접근하는 것 같지만 , 각 쓰레드별로 다른 메모리 위치를 접근하게 됩니다 .
74
/// TLS(Thread Local Storage) 를 사용하여 각 thread 가 자신의 local memory 를 사용하도록 수정하다 .
/// KpSplineUtil namespace 의 함수들은 thread safe 해야 한다 ./// native type 인 경우는 Visual Studio 의 확장 기능인 __declspec( thread ) 를 명시하기만 하면
된다 ./// - jintaeks on 2013-03-20, 13:35
__declspec( thread ) static KpReal s_rBITolerance2;__declspec( thread ) static KpReal s_rBIWidth;__declspec( thread ) static KpReal s_rBIHeight;__declspec( thread ) static KpReal s_rBIntersectionT;__declspec( thread ) static KpReal s_rBMinDist2;
static bool _FindBezierRectangleIntersection( const KpBezierControl& kInBezier_ , KpReal rInTBegin_ , KpReal rInTEnd_ ){ { KpVector3 vMin, vMax; _CalcBezierControlAABB( vMin, vMax, kInBezier_ ); if( vMin.X() > s_rBIWidth || vMax.X() < KpReal( 0.0 )
75
참고문헌 thread tutorial
– http://www.justsoftwaresolutions.co.uk/threading/multithreading-in-c++0x-part-1-starting-threads.html
std::unique_lock– http://www.cplusplus.com/reference/mutex/unique_lock/?
kw=unique_lock– http://en.cppreference.com/w/cpp/thread/unique_lock– http://stackoverflow.com/questions/13099660/c11-why-does-
stdcondition-variable-use-stdunique-lock
std::condition_variable– http://www.cplusplus.com/reference/condition_variable/conditi
on_variable/?kw=condition_variable– http://stackoverflow.com/questions/13099660/c11-why-does-
stdcondition-variable-use-stdunique-lock
76
끝
77
멀티쓰레드 프로그래밍 :03. 쓰레드 라이브러리 2
2014년 11월 3 일[email protected]
목차 index 1회
– 문장 statement 과 표현식 expression– rvalue reference– Move semantics– 쓰레드 thread
2회– std::thread– std::mutex– std::unique_lock– std::condition_variable– TLS
3회– atomic– lock-free– memory ordering– std::atomic<>
79
Atomic연산의 중간 과정을 결과로 얻을 수 없다면 atomic하다고 합니다 .
중간 과정을 얻는 것을 data race 라고 하며 , data race 의 결과 torn read/torn write 가 발생합니다.
non-atomic 이 발생하는 이유는 연산이 여러 개의 Cpu명령으로 분리되기 때문입니다 (하나의 Cpu명령 자체가 atomic 하지 않는 경우도 있습니다 ).
80
Atomic simple type 에 대한 aligned 된 read/write 는 atomic 합니다 .
Win32 의 _InterlockedIncrement(), C++11 의 std::atomic<int>::fetch_add() 는 atomic RMW 의 예입니다 .
std::atomic<> 은 lock-free 를 보장하지 않습니다 . std::atomic<>::is_lock_free() 로 검사해야 합니다.
81
RMW 의 가장 흔한 예는 Compare-And-Swap(CAS) 입니다 .
_InterlockedCompareExchange() 는 Win32 의 CAS 구현 intrinsic 함수입니다 .– intrinsic function 의 의미는 라이브러리가 제공하는 함수가 아니라
컴파일러가 제공하는 함수라는 의미입니다 .
82
Lock-free Programming
일반적으로 mutex 를 쓰지 않는 프로그래밍 기법이라고 알려져 있습니다 .
mutex 의 사용 여부와 상관없이 하나의 쓰레드가 다른 쓰레드를 영구히 block 시킬 수 없다면 lock-free 하다고 합니다 .
83
예 ) lock-free queue 의 push 구현void LockFreeQueue::push(Node* newHead)
{ for (;;){ // Copy a shared variable (m_Head) to a local.
Node* oldHead = m_Head;
// Do some speculative work, not yet visible to other threads.newHead->next = oldHead;
// Next, attempt to publish our changes to the shared variable. // If the shared variable hasn't changed, the CAS succeeds and we return. // Otherwise, repeat. if (_InterlockedCompareExchange(&m_Head, newHead, oldHead) == oldHead) return;}
}
84
_InterlockedCompareExchange() 의 리턴값을 비교하는 짧은 순간에 , 다른 쓰레드가 A값을 B로 바꾼 다음 다시 A로 변경되었을 가능성이 있습니다 .
CAS 는 ABA 문제 (ABA problem) 가 발생하지 않도록 조심스럽게 코딩해야 합니다 .
Memory Ordering
85
Memory Ordering atomic 한 일련의 연산이 보장된다고 , multi-core 에서 동작하는 multi-threaded 프로그램의 data race가 보장되지는 않습니다 .
왜냐하면 compiler 에 의해 , 실행 시간 Cpu 에 의해 연산의 순서가 바뀔 수 있기 때문입니다 .
프로세스는 여러가지 이유로 메모리 연산의 순서order 를 바꿀 수 있습니다 .
86
volatile bool Ready = false;int Value = 0;
// Thread Awhile(!Ready) {}printf("%d", Value);
// Thread BValue = 1;Ready = true;
87
예상되는 Value 의 결과는 1입니다 .하지만 , Ready = true; 가 Value = 1; 보다 먼저 실행된다면 ?
std::atomic<> 에는 6개의 memory ordering 옵션이 있습니다 .
하지만 , 3 가지 memory ordering 모델중 한가지를 나타냅니다 .– squentially-consistent ordering (memory_order_seq_cst)– acquire-release ordering (memory_order_consume,
memory_order_acquire, memory_order_release, and memory_order_acq_rel)
– relaxed ordering (memory_order_relaxed).
88
Memory Barrier pending 된 메모리 연산을 완료하도록 하는 일련의 명령들을 말합니다 .
acquire, release, fence 의 세종류가 있습니다 .
89
Acquire semantics
operation 1 operation 2<-operation 3-Acquire-> 3 is visible before 4-5 operation 4 operation 5
90
데이터에 접근하기 위해서 atomic 연산을 사용할 때, 다른 process 가 변경될 값들의 연산을 실행하기 전에 lock 을 볼 수 있어야 합니다 .
이것을 acquire semantic 이라고 합니다 . 데이터를 접근하기 위한 권한을 얻을려고 acquire 하고 때문입니다 .
Release semantics
operation 1 operation 2<-operation 3-Release-> 1-2 are visible before 3 operation 4 operation 5
91
atomic 연산이 최근에 변경된 값들을 release 하려고 할 때 , 새로운 값은 release 전에 다른 process 에게 보여야 합니다 .
이것을 release semantic 이라고 합니다 .
Fence semantics
operation 1 operation 2<-operation 3-Fence-> 1-2 are visible before 3, 3 is visible before 4-5
operation 4 operation 5
92
fence 는 full memory barrier 라고도 합니다 .
class SpinLock{ volatile tInt LockSem;public: FORCEINLINE SpinLock() : LockSem(0) {} FORCEINLINE tBool Lock() { while(1) { // Atomically swap the lock variable with 1 if it's currently equal to 0 if(!InterlockedCompareExchange(&LockSem, 1, 0)) { // We successfully acquired the lock ImportBarrier(); return; } } } FORCEINLINE void Unlock() { ExportBarrier(); LockSem = 0; }};
93
volatile 로 지정된 변수에 write 하는 것은 release semantic 과 같습니다 .
volotile 로 지정된 변수에서 읽는 것은 acquire semantic 과 같습니다 .
94
예 ) 실제 문제 상황HANDLE beginSema1;HANDLE beginSema2;HANDLE endSema;
int X, Y;int r1, r2;
95
DWORD WINAPI thread1Func(LPVOID param){ MersenneTwister random(1); for (;;) { WaitForSingleObject(beginSema1, INFINITE); // Wait for signal while (random.integer() % 8 != 0) {} // Random delay
// ----- THE TRANSACTION! ----- X = 1;#if USE_CPU_FENCE MemoryBarrier(); // Prevent CPU reordering#else _ReadWriteBarrier(); // Prevent compiler reordering only#endif r1 = Y;
ReleaseSemaphore(endSema, 1, NULL); // Notify transaction complete
} return 0; // Never returns};
DWORD WINAPI thread2Func(LPVOID param){ MersenneTwister random(2); for (;;) { WaitForSingleObject(beginSema2, INFINITE); // Wait for signal while (random.integer() % 8 != 0) {} // Random delay
// ----- THE TRANSACTION! ----- Y = 1;#if USE_CPU_FENCE MemoryBarrier(); // Prevent CPU reordering#else _ReadWriteBarrier(); // Prevent compiler reordering only#endif r2 = X;
ReleaseSemaphore(endSema, 1, NULL); // Notify transaction complete } return 0; // Never returns};
96
#if USE_SINGLE_HW_THREAD // Force thread affinities to the same cpu core. SetThreadAffinityMask(thread1, 1); SetThreadAffinityMask(thread2, 1);#endif
// Repeat the experiment ad infinitum int detected = 0; for (int iterations = 1; ; iterations++) { // Reset X and Y X = 0; Y = 0; // Signal both threads ReleaseSemaphore(beginSema1, 1, NULL); ReleaseSemaphore(beginSema2, 1, NULL); // Wait for both threads WaitForSingleObject(endSema, INFINITE); WaitForSingleObject(endSema, INFINITE); // Check if there was a simultaneous reorder if (r1 == 0 && r2 == 0) { detected++; printf("%d reorders detected after %d iterations\n", detected, iterations); } }
97
어떻게 해결하나요 ?std::atomic<int> X(0), Y(0);int r1, r2;
void thread1(){X.store(1); r1 = Y.load();
}
void thread2(){Y.store(1); r2 = X.load();
}
98
std::atomic<> 은 atomic 연산과 memory barrier 를 지원하는 C++11 의 표준 라이브러리입니다 .
.store() 와 .load() 는 디폴트로 fence 를 설치합니다 .
std::atomic<>
99
Relaxed orderingstd::atomic<int> x;std::atomic<int> y;
// Thread 1:r1 = y.load(memory_order_relaxed); // Ax.store(r1, memory_order_relaxed); // B
// Thread 2:r2 = x.load(memory_order_relaxed); // C y.store(42, memory_order_relaxed); // D
100
is allowed to produce r1 == r2 == 42 because, although A is sequenced-before B and C is sequenced before D, nothing prevents D from appearing before A in the modification order of y, and B from appearing before C in the modification order of x.
예 ) counter#include <vector>#include <iostream>#include <thread>#include <atomic> std::atomic<int> cnt = {0}; void f(){ for (int n = 0; n < 1000; ++n) { cnt.fetch_add(1, std::memory_order_relaxed); }} int main(){ std::vector<std::thread> v; for (int n = 0; n < 10; ++n) { v.emplace_back(f); } for (auto& t : v) { t.join(); } std::cout << "Final counter value is " << cnt << '\n';}
101
Release-Acquire ordering If an atomic store in thread A is tagged std::memory_order_release and an atomic load in thread B from the same variable is tagged std::memory_order_acquire, all memory writes (non-atomic and relaxed atomic) that happened-before the atomic store from the point of view of thread A, become visible side-effects in thread B, that is, once the atomic load is completed, thread B is guaranteed to see everything thread A wrote to memory.
기다리는 B의 atomic load 이후의 명령들이 , A 의 atomic store 전에 처리한 모든 값을 볼 수 있습니다.
102
std::atomic<std::string*> ptr;int data; void producer(){ std::string* p = new std::string("Hello"); data = 42; ptr.store(p, std::memory_order_release);} void consumer(){ std::string* p2; while (!(p2 = ptr.load(std::memory_order_acquire))) ; assert(*p2 == "Hello"); // never fires assert(data == 42); // never fires} int main(){ std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join();}
103
Sequentially-consistent ordering Atomic operations tagged std::memory_order_seq_cst not only order memory the same way as release/acquire ordering (everything that happened-before a store in one thread becomes a visible side effect in the thread that did a load), but also establish a single total modification order of all atomic operations that are so tagged.
core 가 1 개인 경우 명령들이 정렬되어 실행되는 경우와 같습니다 .– fence(full memory barrier) 를 생성합니다 .
104
#include <thread>#include <atomic>#include <cassert> std::atomic<bool> x = {false};std::atomic<bool> y = {false};std::atomic<int> z = {0}; void write_x(){ x.store(true, std::memory_order_seq_cst);} void write_y(){ y.store(true, std::memory_order_seq_cst);}
105
void read_x_then_y(){ while (!x.load(std::memory_order_seq_cst)) ; if (y.load(std::memory_order_seq_cst)) { ++z; }}void read_y_then_x(){ while (!y.load(std::memory_order_seq_cst)) ; if (x.load(std::memory_order_seq_cst)) { ++z; }}int main(){ std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); a.join(); b.join(); c.join(); d.join(); assert(z.load() != 0); // will never happen}
106
107
참고문헌 http://preshing.com/20130618/atomic-vs-non-atomic-operations/– blog series
http://en.cppreference.com/w/cpp/atomic/memory_order
http://www.developerfusion.com/article/138018/memory-ordering-for-atomic-operations-in-c0x/
108
끝
109
멀티쓰레드 프로그래밍 :04. Parallel Pattern Library
2014년 11월 9일[email protected]
목차 index 3회
– atomic– lock-free– memory ordering– std::atomic<>
4 회 : PPL, Parallel Pattern Library– Task Parallelism (Concurrency Runtime)– Parallel Algorithms– Parallel Containers and Objects– Cancellation in the PPL– Debugging a Parallel Program
5회 : C++ AMP 6회 : 각 팀의 Thread 사용 현황
– 각 팀에서 사용중인 쓰레드– PPL 적용 개선 가능한 것들
111
Concurrency Runtime
112
Concurrency Runtime자체의 thread pool 을 유지합니다 . Concurrency runtime 은 work-stealing 알고리즘으로 각 쓰레드의 load 를 조정합니다 .
Concurrency runtime 은 리소스 접근의 동기화를 위해 서로 협동하는 blocking primitive 를 제공합니다 .– Parallel Pattern Library– Asynchronous Agene Library– Task Scheduler– Resource Manager
113
PPL, Parallel Pattern Library Ppl 은 일반적인 목적의 parallel container 와 algorithm 을 제공합니다 .
Ppl 은 parallel algorithm 을 통해 data parallelism 을 제공합니다 .
Ppl 은 task 를 통해 task parallelism 을 제공합니다 .
114
Asynchronous Agent Library Actor-based programming 을 제공합니다 . Message passing interface 를 제공합니다 .아래 링크를 참조하세요 .
– http://msdn.microsoft.com/en-us/library/dd492627.aspx
115
Task Scheduler Task Scheduler 는 실행시간에 task 를 스케쥴링하고 조정 coordinate 합니다 .
Processing 리소스를 최대한으로 사용하기 위해 work-stealing 알고리즘을 사용합니다 .
116
Resource Manager컴퓨팅 리소스 , 즉 프로세서 processor 와 메모리를 관리합니다 .
가장 최적이 되도록 리소스를 할당합니다 . Task Scheduler 와 상호작용하면서 리소스에 대한 추상 계층을 제공합니다 .
117
람다 lambda
In mathematical logic and computer science, lambda is used to introduce anonymous functions expressed with the concepts of lambda calculus.
118
람다 : syntax
a. lambda-introducer (capture clause)
b. lambda declarator (parameter list)
c. mutable (mutable specification)
d. exception-specification (exception specification)
e. trailing-return-type (return type)
f. compound-statement (lambda body)
119
람다 : exampleint main(){ using namespace std;
// Assign the lambda expression that adds two numbers to an auto variable. auto f1 = [](int x, int y) { return x + y; };
cout << f1(2, 3) << endl;
// Assign the same lambda expression to a function object. function<int(int, int)> f2 = [](int x, int y) { return x + y; };
cout << f2(3, 4) << endl;}
120
PPL 의 Task Parallelismparallel pattern library
121
Ppl 예 ) 피보나치 수열 계산// Calls the provided work function and returns the number of milliseconds // that it takes to call that function. template <class Function>__int64 time_call( Function&& f ){ __int64 begin = GetTickCount(); f(); return GetTickCount() - begin;}
// Computes the nth Fibonacci number. int fibonacci( int n ){ if( n < 2 ) return n; return fibonacci( n - 1 ) + fibonacci( n - 2 );}
122
직렬처리 serial processing __int64 elapsed;
// An array of Fibonacci numbers to compute. std::array<int, 4> a = { 24, 26, 41, 42 };
// The results of the serial computation. std::vector<std::tuple<int, int>> results1;
// Use the for_each algorithm to compute the results serially. elapsed = time_call( [&] { std::for_each (std::begin(a), std::end(a), [&]( int n ) { results1.push_back( std::make_tuple( n, fibonacci( n ) ) ); }); }); std::wcout << L"serial time: " << elapsed << L" ms" << std::endl;
123
병렬처리 parallel processing // The results of the parallel computation. concurrency::concurrent_vector<std::tuple<int, int>> results2;
// Use the parallel_for_each algorithm to perform the same task. elapsed = time_call( [&] { concurrency::parallel_for_each( std::begin(a), std::end(a), [&]( int n ) { results2.push_back( std::make_tuple( n, fibonacci( n ) ) ); });
// Because parallel_for_each acts concurrently, the results do not // have a pre-determined order. Sort the concurrent_vector object // so that the results match the serial version. std::sort( std::begin( results2 ), std::end( results2 ) ); }); std::wcout << L"parallel time: " << elapsed << L" ms" << std::endl << std::endl;
124
/** Outputserial time: 9250 msparallel time: 5726 ms
fib(24): 46368fib(26): 121393fib(41): 165580141fib(42): 267914296*/
concurrency::task<>#include <ppltasks.h>#include <iostream>
//using namespace concurrency;//using namespace std;
int wmain(){ // Create a task. concurrency::task<int> t( []() { return 42; });
// In this example, you don't necessarily need to call wait() because // the call to get() also waits for the result. t.wait();
// Print the result. std::wcout << t.get() << std::endl;}
/* Output: 42*/
125
concurrency::task<> 를 사용해 태스크를 정의합니다 . task 의 템플릿 인자는 태스크의 리턴타입 입니다 .
wait() 는 태스크가 실행된 경우 , 태스크의 종료를 기다립니다 .
get() 은 태스크의 종료시 리턴값을 얻습니다 .
concurrency::task::create_task(), then()concurrency::task<std::wstring> write_to_string(){ // Create a shared pointer to a string that is assigned to and read by multiple tasks. // By using a shared pointer, the string outlives the tasks, which can run in the
background after // this function exits. auto s = std::make_shared<std::wstring>(L"Value 1");
return concurrency::create_task([s] { // Print the current value. std::wcout << L"Current value: " << *s << std::endl; // Assign to a new value. *s = L"Value 2";
}).then([s] { // Print the current value. std::wcout << L"Current value: " << *s << std::endl; // Assign to a new value and return the string. *s = L"Value 3"; return *s; });}
126
태스크를 생성하기 위해 create_task() 를 사용합니다 . 연속된 태스크는 task<> 의 then() 을 사용합니다 .
lambda 는 thread-safe 해야 합니다 .// lambda-task-lifetime.cpp // compile with: /EHsc#include <ppltasks.h>#include <iostream>#include <string>
…int wmain(){ // Create a chain of tasks that work with a string. auto t = write_to_string();
// Wait for the tasks to finish and print the result. std::wcout << L"Final value: " << t.get() << std::endl;}
/* Output: Current value: Value 1 Current value: Value 2 Final value: Value 3*/127
태스크의 동작은 thread-safe 해야 합니다 . 그러므로 람다함수는 thread-safe 한 동작이 보장되도록 적절하게 변수를 capture 해야 합니다 .
예에서 string 은 write_to_string() 이 리턴된 이후에도 유효해야 하므로 std::shared_ptr<> 로 관리하고 있습니다 .
concurrency::task<std::array<std::array<int, 10>, 10>> create_identity_matrix([] { std::array<std::array<int, 10>, 10> matrix; int row = 0; std::for_each( std::begin(matrix), std::end(matrix), [&row](std::array<int, 10>& matrixRow) { std::fill( std::begin(matrixRow), std::end(matrixRow), 0); matrixRow[row] = 1; row++; }); return matrix; });
auto print_matrix = create_identity_matrix.then([](std::array<std::array<int, 10>, 10> matrix) { std::for_each( std::begin(matrix), std::end(matrix), [](std::array<int, 10>& matrixRow) { std::wstring comma; std::for_each( std::begin(matrixRow), std::end(matrixRow), [&comma](int n) { std::wcout << comma << n; comma = L", "; }); std::wcout << std::endl; }); });
128
then()
int wmain(){ … print_matrix.wait();}/* Output: 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 0, 0, 0, 0, 0, 0, 0, 1, 0, 0 0, 0, 0, 0, 0, 0, 0, 0, 1, 0 0, 0, 0, 0, 0, 0, 0, 0, 0, 1*/
129
auto create_identity_matrix = concurrency::create_task([] { std::array<std::array<int, 10>, 10> matrix; int row = 0; std::for_each( std::begin(matrix), std::end(matrix), [&row](std::array<int,
10>& matrixRow) { std::fill( std::begin(matrixRow), std::end(matrixRow), 0); matrixRow[row] = 1; row++; }); return matrix; });
concurrency::task 의 타입을 명시적으로 정의하기보다는 auto 로 정의하고 , create_task() 를 이용해서 정의합니다 .
task continuationint wmain(){ auto t = concurrency::create_task([]() -> int { return 0; });
// Create a lambda that increments its input value. auto increment = [](int n) { return n + 1; };
// Run a chain of continuations and print the result. int result = t.then(increment).then(increment).then(increment).get(); std::wcout << result << std::endl;}
/* Output: 3*/
130
task in taskint wmain(){ auto t = concurrency::create_task([]() { std::wcout << L"Task A" << std::endl;
// Create an inner task that runs before any continuation // of the outer task. return concurrency::create_task([]() { std::wcout << L"Task B" << std::endl; }); });
// Run and wait for a continuation of the outer task. t.then([]() { std::wcout << L"Task C" << std::endl; }).wait();}
131
/* Output: Task A Task B Task C*/
when_all()int wmain(){ // Start multiple tasks. std::array<concurrency::task<void>, 3> tasks = { concurrency::create_task([] { std::wcout << L"Hello from taskA." << std::endl; }), concurrency::create_task([] { std::wcout << L"Hello from taskB." << std::endl; }), concurrency::create_task([] { std::wcout << L"Hello from taskC." << std::endl; }) };
auto joinTask = concurrency::when_all( std::begin(tasks), std::end(tasks) );
// Print a message from the joining thread. std::wcout << L"Hello from the joining thread." << std::endl;
// Wait for the tasks to finish. joinTask.wait();}
132
/* Sample output: Hello from the joining thread. Hello from taskA. Hello from taskC. Hello from taskB.*/
when_all 은 task<std::vector<T>>를 리턴합니다 .
when_all() : get returnsint wmain(){ // Start multiple tasks. std::array<concurrency::task<int>, 3> tasks = { concurrency::create_task([]() -> int { return 88; }), concurrency::create_task([]() -> int { return 42; }), concurrency::create_task([]() -> int { return 99; }) };
auto joinTask = concurrency::when_all( std::begin(tasks), std::end(tasks) ).then([]( std::vector<int> results )
{ std::wcout << L"The sum is " << std::accumulate( std::begin(results), std::end(results), 0 ) << L'.' << std::endl; });
// Print a message from the joining thread. std::wcout << L"Hello from the joining thread." << std::endl;
// Wait for the tasks to finish. joinTask.wait();}133
/* Output: Hello from the joining thread. The sum is 229.*/
when_any()int wmain(){ // Start multiple tasks. std::array<concurrency::task<int>, 3> tasks = { concurrency::create_task([]() -> int { return 88; }), concurrency::create_task([]() -> int { return 42; }), concurrency::create_task([]() -> int { return 99; }) };
// Select the first to finish. concurrency::when_any( std::begin(tasks), std::end(tasks)).then([]( std::pair<int, size_t>
result) { std::wcout << "First task to finish returns " << result.first << L" and has index " << result.second << L'.' << std::endl; }).wait();}
134
/* Sample output: First task to finish returns 42 and has index 1.*/
when_any() 는 최초로 완료된 태스크의 std::pair< 리턴값 ,index> 를 리턴합니다 .
task group태스크의 집합을 관리합니다 .
– 태스크 그룹은 태스크를 work-stealing 큐에 push 합니다 .– 그룹의 각 태스크는 concurrency::task_handl 로 접근합니다 .
structured task group– 그룹의 연산은 같은 쓰레드에서 일어나야 합니다 .• cancel() 과 is_cancelling() 만 예외입니다 .
– wait() 호출 이후에 태스크를 추가하면 안 됩니다 .– 쓰레드 간의 동기화를 하지 않으므로 task_group 보다 오버헤드가
적습니다 .
unstructured task group concurrency::parallel_invoke() 는 structured_task_group 을 사용합니다 .
태스크 그룹은 cancellation 을 지원합니다 .135
structured_task_groupint wmain(){ // Use the make_task function to define several tasks. auto task1 = concurrency::make_task([] { /*TODO: Define the task body.*/ }); auto task2 = concurrency::make_task([] { /*TODO: Define the task body.*/ }); auto task3 = concurrency::make_task([] { /*TODO: Define the task body.*/ });
// Create a structured task group and run the tasks concurrently.
concurrency::structured_task_group tasks;
tasks.run( task1 ); tasks.run( task2 ); tasks.run_and_wait( task3 );}
136
make_task 는 task 를 정의만 하고 실행하지 않습니다 .
structured task group 의 run_and_wait() 는 group 의 모든 task 가 종료하기를 기다립니다 .
parallel_invoketemplate <typename T>T twice( const T& t ){ return t + t;}
int wmain(){ // Define several values. int n = 54; double d = 5.6; std::wstring s = L"Hello";
// Call the twice function on each value concurrently. // parallel_invoke uses structured_task_group internally. jintaeks on 20141107 concurrency::parallel_invoke( [&n] { n = twice(n); }, [&d] { d = twice(d); }, [&s] { s = twice(s); } );
// Print the values to the console. std::wcout << n << L' ' << d << L' ' << s << std::endl;}137
/** Output 108 11.2 HelloHello*/
parallel_invoke() 는 내부적으로 structured_task_group 을 이용합니다 .
unstructured task_groupint wmain(){ // A task_group object that can be used from multiple threads. concurrency::task_group tasks;
// Concurrently add several tasks to the task_group object. concurrency::parallel_invoke( [&] { // Add a few tasks to the task_group object. tasks.run([] { print_message(L"Hello"); }); tasks.run([] { print_message(42); }); }, [&] { // Add one additional task to the task_group object. tasks.run([] { print_message(3.14); }); } );
// Wait for all tasks to finish. tasks.wait();}
138
/** Output: Message from task: Hello Message from task: 3.14 Message from task: 42*/
non-structured task group 는 서로 다른 thread 에서 group 를 접근해서 task를 관리할 수 있습니다.
Cancellation concurrency::cancellation_token_source cts; auto token = cts.get_token();
std::wcout << L"Creating task..." << std::endl;
// Create a task that performs work until it is canceled. auto t = concurrency::create_task( [] { bool moreToDo = true; while( moreToDo ) { // Check for cancellation. if( concurrency::is_task_cancellation_requested() ) { // TODO: Perform any necessary cleanup here...
// Cancel the current task. concurrency::cancel_current_task(); } else { // Perform work. moreToDo = do_work(); } } }, token );
139
// Wait for one second and then cancel the task. concurrency::wait( 1000 );
std::wcout << L"Canceling task..." << std::endl; cts.cancel();
// Wait for the task to cancel. std::wcout << L"Waiting for task to complete..." << std::endl; t.wait();
std::wcout << L"Done." << std::endl;
140
/* Sample output: Creating task... Performing work... Performing work... Performing work... Performing work... Canceling task... Waiting for task to complete... Done.*/
Cancellation callback concurrency::cancellation_token_source cts; auto token = cts.get_token();
// An event that is set in the cancellation callback. concurrency::event e;
concurrency::cancellation_token_registration cookie; cookie = token.register_callback( [&e, token, &cookie]() { std::wcout << L"In cancellation callback..." << std::endl; e.set();
// Although not required, demonstrate how to unregister // the callback. token.deregister_callback(cookie); } );
141
std::wcout << L"Creating task..." << std::endl;
// Create a task that waits to be canceled. auto t = concurrency::create_task([&e] { e.wait(); }, token );
// Cancel the task. std::wcout << L"Canceling task..." << std::endl; cts.cancel();
// Wait for the task to cancel. t.wait();
std::wcout << L"Done." << std::endl;
142
/* Sample output: Creating task... Canceling task... In cancellation callback... Done.*/
Task tree // Create a task group that serves as the root of the tree. concurrency::structured_task_group tg1;
// Create a task that contains a nested task group. auto t1 = concurrency::make_task([&] { concurrency::structured_task_group tg2;
std::wcout << L"t1 task" << std::endl;
// Create a child task. auto t4 = concurrency::make_task([&] { std::wcout << L"t4 task" << std::endl; });
// Create a child task. auto t5 = concurrency::make_task([&] { std::wcout << L"t5 task" << std::endl; });
// Run the child tasks and wait for them to finish. tg2.run(t4); tg2.run(t5); tg2.wait(); });143
// Create a child task. auto t2 = concurrency::make_task([&] { std::wcout << L"t2 task" << std::endl; });
// Create a child task. auto t3 = concurrency::make_task([&] { std::wcout << L"t3 task" << std::endl; });
// Run the child tasks and wait for them to finish. tg1.run(t1); tg1.run(t2); tg1.run(t3); tg1.wait();
144
PPL 응용
145
Bitonic Merge Sort http://msdn.microsoft.com/en-us/library/vstudio/dd728066(v=vs.110).aspx
146
현재 Kog 게임에 적용 , FrameMove
147
148
엘소드 Npc 의 OnFrameMove() 는 약 2700 줄 ㅡㅡ ;
Debugging Parallel Tasks
149
예 ) create_task_tree() // Create a task group that serves as the root of the tree. concurrency::structured_task_group tg1;
// Create a task that contains a nested task group. auto t1 = concurrency::make_task([&] { concurrency::structured_task_group tg2;
std::wcout << L"t1 task" << std::endl;
// Create a child task. auto t4 = concurrency::make_task([&] { std::wcout << L"t4 task" << std::endl; });
// Create a child task. auto t5 = concurrency::make_task([&] { std::wcout << L"t5 task" << std::endl; });
// Run the child tasks and wait for them to finish. tg2.run(t4); tg2.run(t5); tg2.wait(); });150
// Create a child task. auto t2 = concurrency::make_task([&] { std::wcout << L"t2 task" << std::endl; });
// Create a child task. auto t3 = concurrency::make_task([&] { std::wcout << L"t3 task" << std::endl; });
// Run the child tasks and wait for them to finish. tg1.run(t1); tg1.run(t2); tg1.run(t3); tg1.wait();
151
Visual Studio 2012 Parallel 디버거
152
Parallel Task Window tg1 의 t1 에 breakpoint 가 활성화된 상황입니다 .
153
Parallel Task Window : task_group tg1 의 3 개의 task 중 2 개가 “활성” 상태입니다 .
154
Parallel Callstack Window “ 활성” 상태인 2개의 task 가 2 개의 쓰레드에서 실행되고 있습니다 .
155
Parallel Callstack Window : Task “ 작업 task” 단위로 callstack 을 관찰 할 수 있습니다 .
156
Parallel Watch같은 변수의 각 thread 에서의 값을 관찰합니다 .
157
데모
158
참고문헌 http://msdn.microsoft.com/en-us/library/dd492418.aspx
http://www.danielmoth.com/Blog/Parallel-Tasks-New-Visual-Studio-2010-Debugger-Window.aspx
http://channel9.msdn.com/Events/Windows-Camp/Developing-Windows-8-Metro-style-apps-in-Cpp/Async-made-simple-with-Cpp-PPL
http://en.wikipedia.org/wiki/Bitonic_sorter http://msdn.microsoft.com/en-us/library/dd554943.aspx
159
끝
160