시즌 2 : 멀티쓰레드 프로그래밍이 왜 이리 힘드나요? 
(Lock-free에서 Transactional Memory까지) 
정내훈 
한국산업기술대학교 게임공학부
발표자 소개 
KAIST 전산과 박사 
−전공 : 멀티프로세서 CPU용 일관성 유지 HW 
NCSoft 근무 
−Alterlife 프로그램 팀장 
−Project M(현 Blade & Soul) 프로그램 팀장 
−CTO 직속 게임기술연구팀 
현 : 한국산업기술대학교 게임공학과 부교수 
−학부 강의 : 게임서버프로그래밍, 멀티코어프로그래밍 
−대학원 강의 : 멀티코어프로그래밍, 심화 게임서버 프로그래밍 
2-2
참고 
삼성 첨단기술연수소와 CJ E&M에서 강의중인 내용 반영 
−40시간 강의 (실습 포함) => 뒷부분 만 발췌 
대학원 4주 강의 분량의 압축 
2-3
2-4 
목차 
도입 : 그래도 멀티쓰레드 프로그램을 하시려구요? 
대책 
성능 
미래 또는 현실
도입 
제가 하는 발표 들어본 적이 있으신 분? 
멀티쓰레드 프로그래밍 경험 있으신 분? 
Lock-free 자료 구조가 무엇인지 아시는 분? 
2-5 
DEVIEW 2013 
CJ E&M, NDC, KGC, 삼성, JCE
도입 
멀티쓰레드 프로그래밍의 위험성 
−“자꾸 죽는데 이유를 모르겠어요” 
자매품 : “이상한 값이 나오는데 이유를 모르겠어요” 
−“더 느려져요” 
2-6 
[미] MuliThreadProgramming [mʌ́ltiθred-|proʊgrӕmɪŋ] : 1. 흑마술, 마공 2. 위력이 강대하나 다루기 어려워 잘 쓰이지 않는 기술
도입 
멀티쓰레드 프로그래밍의 어려움 (한페이지 요약) 
−Data Race : 2를 5천만번 더했는데 1억이 안 나오는 경우. 
Lock을 사용해 해결 
−성능 : 싱글 쓰레드 버전보다 더 느림 
Lock 쓰지 말자 
−컴파일러 : 변수를 참조했는데, 컴파일러가 무시 
volatile 키워드로 해결, atomic으로 해결 
−CPU : 프로그램 실행순서를 자기 마음대로 변조 
asm mfence; 명령으로 해결, atomic으로 해결 
−Cache : -1을 썼는데 65535가 써짐 
포인터 주소 확인하기. 
2-7 
ABA 문제는?
목차 
도입 
대책 : Lock-free 프로그래밍이 도대체 뭐야? 
성능 
미래 또는 현실 
2-8
대책 
현실의 멀티쓰레드 프로그램은? 
−멀티 코어에서 여러 쓰레드가 동시에 실행된다. 
−쓰레드간의 데이터 공유 및 동기화는 안전한 Lock-free 자료구조를 통해서 이루어진다. 
−언리얼 3 : 디스플레이 리스트 Queue 
−각종 게임 서버 : Job Queue외 다수 
2-9
대책 
성능을 위해서는 Lock-free 알고리즘을 사용하여야 한다. 
사용하지 않으면 
−병렬성 감소 
−Priority Inversion 
−Convoying 
−/* 성능이 떨어지고 랙이 발생한다 */ 
−/* 작년에 보여드렸어요~~ */ 
2-10
대책 
Lock-free 알고리즘이란? 
−여러 개의 쓰레드에서 동시에 호출했을 때에도 정해진 단위 시간마다 적어도 한 개의 호출이 완료되는 알고리즘. 
??????
대책 
Lock-free 알고리즘이란? 
−자료구조 및 그것에 대한 접근 방법 
예) QUEUE : enqueue, dequeue 
예) STACK : push, pop 
예) 이진 트리 : insert, delete, search 
2-12
대책 
Lock-free 알고리즘이란? 
−멀티쓰레드에서 동시에 호출해도 정확한 결과를 만들어 주는 알고리즘 
STL 탈락. 
−Non-Blocking 알고리즘 
다른 쓰레드가 어떤 상태에 있건 상관없이 호출이 완료된다. 
Lock 사용 시 탈락. 
−다른 쓰레드와 충돌하였을 경우에도 언제나 적어도 하나의 승자가 있어서, 승자는 delay없이 완료됨.
대책 
(보너스) 
−Wait-free 알고리즘은? 
호출이 다른 쓰레드와 충돌해도 모두 delay없이 완료 된다. 
추가 상식 
−LOCK을 사용하지 않는다고 lock-free 알고리즘이 아니다!!! 
−LOCK을 사용하면 무조건 lock-free알고리즘이 아니다.
대책 
알고리즘의 분류 
2-15 
알고리즘 
싱글쓰레드 
멀티쓰레드 
Blocking 
Non-blocking 
Lock-free 
Wait-free 
…. 
….
대책 
예) Blocking 알고리즘 
2-16 
mylock.lock(); sum = sum + 2; mylock.unlock(); 
BLK_QUEUE::push(int x) { Node *e = New_Node(x); qlock.lock(); tail->next = e; tail = e; glock.unlock(); } 
while (dataReady == false); 
_asm mfence; 
temp = g_data;
대책 
왜 Blocking인가? 
−dataReady에 true가 들어가지 않으면 이 알고리즘은 무한 대기, 즉 다른 쓰레드에서 무언가 해주기를 기다린다. 
−여러 가지 이유로 dataReady에 true가 들어오는 것이 지연될 수 있다. 
Schedule out, 다른 쓰레드 때문에 대기 
2-17 
while (dataReady == false); _asm mfence; temp = g_data;
대책 
Non-blocking은? 
2-18 
_asm lock add sum, 2; 
LF_QUEUE::push(int x) { 
Node *e = new_Node(x); 
while (true) { 
Node *last = tail; 
Node *next = last->next; 
if (last != tail) continue; 
if (NULL == next) { 
if (CAS(&(last->next), NULL, e)) { 
CAS(&tail, last, e); 
return; 
} 
} else CAS(&tail, last, next); 
} } 
BLK_QUEUE::push(int x) { Node *e = New_Node(x); qlock.lock(); tail->next = e; tail = e; glock.unlock(); }
대책 
Non-blocking은? 
2-19 
if (dataReady == false) return false; _asm mfence; temp = g_data; 
while (dataReady == false); 
_asm mfence; 
temp = g_data;
대책 
CAS가 보이는데??? 
−Lock-free 알고리즘의 핵심 
−CAS가 없이는 대부분의 non-blocking 알고리즘들을 구현할 수 없다. 
Queue, Stack, List… 
−CAS를 사용하면 모든 싱글쓰레드 알고리즘 들을 Lock-free 알고리즘으로 변환할 수 있다!!! 
2-20
대책 
CAS 
−CAS(&A, old, new); 
−의미 : A의 값이 old면 new로 바꾸고 true를 리턴, 아니면 false를 리턴. 
−다른 버전의 의미 : A메모리를 다른 쓰레드가 먼저 업데이트 해서 false가 나왔다. 모든 것을 포기하라. 
2-21
대책 
Lock-free 알고리즘은 어떻게 구현되는가? 
알고리즘의 동작이란? 
−기존의 자료구조의 구성을 다른 구성으로 변경하거나 자료구조에서 정보를 얻어내는 행위 
2-22 
3 
Head 
Tail 
1 
9 
X 
3 
Head 
Tail 
1 
9 
X 
push(35); 
35
대책 
Lock-free 알고리즘은 어떻게 구현되는가? 
2-23 
자료구조를 변경을 시도한다. 
성공했는가? 
완료 
no 
yes 
(time machine) 시도 전으로 되돌아간다.. 
??? 
???
대책 
Lock-free 알고리즘은 어떻게 구현되는가? 
앞의 알고리즘이 불가능 하므로 
2-24 
자료구조의 변경을 시도한다. but, 다른 쓰레드가 먼저 변경했으면 시도 취소. 
성공했는가? 
완료 
yes 
no 
현재의 자료구조를 파악한다.
대책 
2-25 
자료구조의 변경을 시도한다. but, 다른 쓰레드가 먼저 변경했으면 시도 취소. 
CAS 
while (true) { int old_sum = sum; if (true == CAS(&sum, old_sum, old_sum+2)) break; } 
mylock.lock(); sum = sum + 2; mylock.unlock(); 
while (CAS(&lock, 0, 1)); sum = sum + 2; lock = 0; 
차이점?
대책 
2-26 
자료구조의 변경을 시도한다. 
but, 다른 쓰레드가 먼저 변경했으면 시도 취소. 
CAS 
LF_QUEUE::push(int x) { Node *e = New_Node(x); while (true) { Node *last = tail; Node *next = last->next; if (last != tail) continue; if (NULL != next) continue; if (CAS(&(last->next), NULL, e, &tail, last, e)) return; } } 
BLK_QUEUE::push(int x) { Node *e = New_Node(x); qlock.lock(); tail->next = e; tail = e; glock.unlock(); }
대책 
2-27 
현실 
LF_QUEUE::push(int x) { Node *e = New_Node(x); while (true) { Node *last = tail; Node *next = last->next; if (last != tail) continue; if (NULL != next) continue; if (CAS(&(last->next), NULL, e, &tail, last, e)) return; } } 
LF_QUEUE::push(int x) { Node *e = New_Node(x); while (true) { Node *last = tail; Node *next = last->next; if (last != tail) continue; if (NULL == next) { if (CAS(&(last->next), NULL, e)) { CAS(&tail, last, e); return; } } else CAS(&tail, last, next); } } 
하지만 2개의 변수에 동시에 CAS를 적용할 수 는 없다!
대책 
… 
−알고리즘이 많이 복잡하다. 
−그래서 작성시 실수하기가 쉽다. 
−실수를 적발하기가 어렵다. 
하루에 한두 번 서버 크래시 
가끔 가다가 아이템 증발 
−제대로 동작하는 것이 증명된 알고리즘을 사용해야 한다. 
2-28
대책 
Lock-Free 알고리즘의 장단점 
−장점 
성능!! 
높은 부하에도 안정적!! 
−단점 
생산성 : 복잡한 알고리즘 
신뢰성 : 정확성을 증명하는 것은 어렵다. 
확장성 : 새로운 메소드를 추가하는 것이 매우 어렵다. 
메모리 : 메모리 재사용의 관리가 어렵다. ABA문제 
2-29
대책 
결론 
−믿을 수 있는 non-blocking container들을 사용하라. 
Intel TBB, Visual Studio PPL 
−자신을 포함한 출처가 의심스러운 알고리즘은 정확성을 증명하고 사용하라. 
정확성이 증명된 논문에 있는 알고리즘은 OK. 
2-30
목차 
도입 
대책 
성능 : 데스크탑에서 동접 1만 서버를 만들어 보자. 
미래 또는 현실 
2-31
성능 
간단한 MMORPG 서버를 만들어 보자 
−1000x1000 world 
−60x60 sector 
−시야 30 
−1000마리의 몬스터 
플레이어 접근 시 자동 이동 및 공격 
−이동/공격/채팅 가능 
−Windows에서 IOCP로 구현 
−<2013년 게임서버프로그래밍 텀프로젝트 by 임상수>
성능 
성능 향상을 위해 
−시야 처리시 검색 성능을 위해 월드를 sector로 분할하여 주위 sector만 검색 
병렬 검색을 위해 tbb::concurrent_hash_map 사용 
−몬스터 AI 처리를 모든 쓰레드에서 균등하게 나누어 처리하기 위해 timer와 event시스템 사용 
timer queue 병렬 등록을 위해 tbb::concurrent_priority_queue를 사용 
−객체 id의 재사용을 막고 메모리 재사용을 위해 객체 id와 객체 배열의 인덱스를 쌍으로 관리 
 <id, index>의 병렬 검색을 위해 tbb::concurrent_hash_map 사용
성능 
성능 측정용 DummyClient 
−서버에게 부하를 걸 수 있도록 여러 명의 Player를 에뮬레이션 해주는 프로그램 
−사람이 플레이 하는 것과 비슷하게 Player를 주기적으로 랜덤한 방향으로 이동 시킴 
−한명의 유저당 하나의 소켓 연결을 하므로 서버에서는 일반 클라이언트 접속과 DummyClient접속이 서로 차이가 없음 
−훌륭한 UNIT TESTER 
−/* 이것도 IOCP로 구현, Direct3D로 유저 분포 실시간 디스플레이 */
성능 
2-35
성능 
실행 결과 
−Intel i7 920 2.67GHz 머신에서 실행 
동접 8000 정도까지 
−Lock-free 자료구조로 최적화 하기 전에는 동접 3000 정도가 한계.
목차 
도입 
대책 
성능 
미래 또는 현실 : Transactionl 메모리, 그리고… 
2-37
현실 
C++11 
−멀티쓰레드 프로그래밍 API의 표준화 
−멀티 쓰레드 표준 라이브러리 
 <thread>, <mutex>, <atomic>, <memory> 
−Boost ASIO 
−G++ 4.8, Visual Studio 2013 
아직 최적화에 문제 : shared_ptr, mutex
현실 
멀티쓰레드 프로그래밍 도우미 (1/2) 
−Intel TBB 
좋은 성능, 유용한 라이브러리 
Concurrent 자료 구조 
−Visual Studio PPL 
Intel TBB와 유사 
−OpenMP 
컴파일러 레벨에서 병렬 프로그램 API를 제공 
성능과 골치 아픈 문제들은 그대로 
−그 외, CK(Concurrency Kit), Noble Library
현실 
암담한 현실 
−Blocking Algorithm 
성능 저하, priority inversion, convoying 
Deadlock! 
−Non-blocking Algorithm 
높은 제작/검증 난이도 
어쩌라고???? 
Transactional Memory!
현실 
Transactional Memory? 
2-41 
Tim Sweeny at GameTech2010
현실 
Transactional Memory는 
−아까 본 바로 그 그림을 그대로 구현한 것… 
자료구조를 변경한다. 
다른 쓰레드와 충돌했는가? 
완료 
no 
yes 
(time machine) 시도 전으로 되돌아간다.. 
???
현실 
Transactional Memory가 좋은 이유? 
−생산성! 
−싱글쓰레드 프로그램을 그대로 쓸 수 있음 
하지만 멀티쓰레드에서 Lock-free하게 실행됨. 
LF_QUEUE::push(int x) { Node *e = new Node(x); atomic { tail->next = e; tail = e; } } 
LF_QUEUE::push(int x) { Node *e = New_Node(x); while (true) { Node *last = tail; Node *next = last->next; if (last != tail) continue; if (NULL == next) { if (CAS(&(last->next), NULL, e)) { CAS(&tail, last, e); return; } } else CAS(&tail, last, next); } } 
Transactionl Memory 구현
현실 
Transactional Memory의 사용법 
−기존의 싱글쓰레드 프로그램에서 다른 쓰레드와 충돌이 일어날 수 있는 구간을 transaction으로 선언한다. 
끝, 이게 전부
현실 
어떻게 가능한가??? 
1. transaction 구간에서 읽고 쓴 메모리를 다른 쓰레드에서 접근했는지 검사한다. 
write는 실제 메모리에 쓰지 않고 잠시 대기 
2. 다른쓰레드에서의 접근이 있었으면 모든 업데이트를 무효화 한 후 다시 시도. 
3. 다른쓰레드에서의 접근이 없었다면 transaction 구간에서의 update를 실제 메모리에 update
현실 
그게 구현 가능하다고?? 
어떻게 
−Software적으로 모든 공유 메모리 업데이트를 관리하면 됨 (STM: Software Transactional Memory) 
−또는, 하드웨어가 알아서 해줌 (HTM: Hardware Transactional Memory) 
2-46
현실 
Transactional Memory 
장점 
−높은 생산성 
기존의 프로그램을 그대로 사용 가능 
단점 
−STM : 오버헤드로 인한 속도 저하 
−HTM : HW 필요. 
한계점 
−쓰레드가 너무 많을 경우 잦은 충돌로 인한 성능향상의 한계 
2-47
현실 
Transactional Memory 
지금까지는 꿈속의 개념이고 8 Core이상에서 STM의 성능을 기대하는 정도였으나. 
Intel에서 일을 저지름. 
2-48 
2013년 6월 HASWELL말매 Haswell은 HTM을 지원.
현실 
Intel Haswell 
2-49
트랜잭션 메모리의 구현 
Haswell의 HTM 
−복수개의 메모리에 대한 Transaction을 허용한다. 
L1 Data Cache의 크기 만큼 
−CPU에서 transaction 실패시의 복구를 제공한다. 
메모리와 레지스터의 변경을 모두 Roll-back한다. 
−Visual Studio 2012, update 2에서 지원 
2-50
트랜잭션 메모리의 구현 
하드웨어 트랜잭션 메모리 예제 
2-51 
DWORD WINAPI ThreadFunc(LPVOID lpVoid) { for (int i=0;i<500000000 / num_thread;++i) { while (_xbegin() != _XBEGIN_STARTED) _xabort(0); sum += 2; _xend(); } return 0; }
트랜잭션 메모리의 구현 
하드웨어 트랜잭션 메모리 예제 (Set의 Add) 
2-52 
bool Add(int key) { NODE *pred, *curr; NODE *node = new NODE(key); while(true) { pred = &head; curr = pred->next; while (curr->key < key) { pred = curr; curr = curr->next; } if (_XBEGIN_STARTED != my_xbegin()) { _xabort(0); continue; } if (!validate(pred, curr)) { _xabort(0); continue; } if (key == curr->key) { _xend(); delete node; return false; } else { node->next = curr; pred->next = node; _xend(); return true; } } } 
HTM 
Lock-Free
트랜잭션 메모리의 구현 
Haswell HTM의 한계 
−모든 알고리즘에 적용 불가능 
HW 용량 한계 => 알고리즘의 맞춤형 수정 필요. 
Nested Transaction 불가능 => 가능은 한데… 
−오버헤드 
모든 레지스터 내용 저장 및 Roll-back 
2-53 
그리고…
트랜잭션 메모리의 구현 
2-54
트랜잭션 메모리의 구현 
2-55 
T_T
미래 
HTM이 업그레이드 되어서 보급되면 끝인가? 
−쓰레드가 많아 질 수록 충돌확률이 올라가 TM의 성능이 떨어진다. 
−64Core 정도가 한계일 것이라고 예측하고 있다. (2010 GameTech, Tim Sweeny)
미래 
왜 쓰레드사이에 충돌이 생기는가? 
−C 스타일 언어를 사용하기 때문이다. 
공유 메모리 
side effect 
해결책은? C 비슷한 언어를 버린다. 
−대신 함수형, 논리형 언어 사용 
공유 메모리 없고 side effect없음
새로운 언어 
주목받고 있는 언어 
−하스켈 
순수 함수형 언어로 1990년에 개발 
개념은 뛰어나나 난이도로 인해 많이 사용되지 못하고 있음. 
−Earlang 
에릭슨에서 전자 계산기용으로 1982년에 개발 
Scalable한 서버 시스템에 자주 사용되고 있음 
−Go, RUST 
2010GDC
새로운 언어 
함수형 언어의 프로그래밍 스타일 
−모든 변수가 불변이다. (C++의 const) 
불변 : 한번 값이 정해지면 바뀌지 않음. 
−불변 변수(immutable variable)는 data race를 일으키지 않는다.
새로운 언어 
하스켈 
−순수 함수형 언어로 1990년에 개발 
−개념은 뛰어나나 난이도로 인해 많이 사용되지 못하고 있음. 
−병렬성 
순수 병렬성 (Parallelism) : 언어에 내재된 병렬성 이용, 항상 같은 결과값, data race나 deadlock이 전혀 없음, I/O처리 안됨 
동시 실행성 (Concurrency) : I/O 처리를 위해 사용. I/O 실행순서 제어는 프로그래머가 해줘야 함. data race나 deadlock이 가능. 
−성능 문제 : Lazy Evaluation, Fine Grained Execution
새로운 언어 
Erlang 
−에릭슨에서 전자 교환기용으로 1982년에 개발 
−Scalable한 서버 시스템에 자주 사용되고 있음 
2010GDC
새로운 언어 
Erlang(사용예) (from wikipedia) 
−Amazon.com : to implement SimpleDB, providing database services as a part of the Amazon Web Services offering 
−Facebook : to power the backend of its chat service, handling more than 100 million active users 
−WhatsApp : to run messaging servers, achieving up to 2 million connected users per server. 
−GitHub : used for RPC proxies to ruby processes 
−CouchDB, Couchbase Server, Mnesia, Riak, SimpleDB
새로운 언어 
Erlang 
−process 단위의 병렬성 
OS process(x), thread(x), SW context(o) 
수천만개의 process 동시 실행 가능 
−2006년부터 SMP(multicore) 지원 
−process사이의 동기화 
shared-nothing asynchronous message passing으로 구현. 
queue통한 message passing으로 동기화 
–분산 처리 가능
새로운 언어 
Erlang : 분산 I/O 구현 예 
% Create a process and invoke the function % web:start_server(Port, MaxConnections) ServerProcess = spawn(web, start_server, [Port, MaxConnections]), % Create a remote process and invoke the function % web:start_server(Port, MaxConnections) on machine RemoteNode RemoteProcess = spawn(RemoteNode, web, start_server, [Port, MaxConnections]), % Send a message to ServerProcess (asynchronously). % The message consists of a tuple with the atom "pause" and the number "10". ServerProcess ! {pause, 10}, % Receive messages sent to this process receive a_message -> do_something; {data, DataContent} -> handle(DataContent); {hello, Text} -> io:format("Got hello message: ~s", [Text]); {goodbye, Text} -> io:format("Got goodbye message: ~s", [Text]) end. 
http://en.wikipedia.org/wiki/Erlang_(programming_language)
새로운 언어 
함수형 언어의 문제 
−생산성 
전혀 다른 스타일의 프로그래밍 
−성능 
구조체를 함수에 전달할 때 포인터가 아니라 내용을 전달하는 것과 비슷한 오버헤드. 
너무 자잘한 병렬화 
−IO 문제 
IO는 순서대로 행해져야 하는데 함수형 언어에서 IO Operation의 순서를 정해주는 것이 비효율적이다.
정리 
Lock-free 알고리즘이 무엇인가? 
−어떤 조건을 만족해야 하는가? 
−어떻게 구현되는가? 
−왜 어려운가? 
−하지만 왜 써야만 하는가. 
멀티쓰레드 프로그래밍의 미래. 
−Transactional Memory 
진격의 INTEL 
−새로운 언어의 필요 
2-66
NEXT 
다음 주제(내년???) 
−Lock-free search : SKIP-LIST 
−ABA Problem, aka 효율적인 reference counting 
−고성능 MMO서버를 위한 non-blocking 자료구조의 활용 
2-67
Q&A 
연락처 
−nhjung@kpu.ac.kr 
−발표자료 : ftp://210.93.61.41 id:ndc21 passwd: <바람의나라> 
−Slideshare에 올릴 예정. 
참고자료 
−Herlihy, Shavit, “The Art of Multiprocesor Programming, Revised”, Morgan Kaufman, 2012 
2-68

[2B7]시즌2 멀티쓰레드프로그래밍이 왜 이리 힘드나요

  • 1.
    시즌 2 :멀티쓰레드 프로그래밍이 왜 이리 힘드나요? (Lock-free에서 Transactional Memory까지) 정내훈 한국산업기술대학교 게임공학부
  • 2.
    발표자 소개 KAIST전산과 박사 −전공 : 멀티프로세서 CPU용 일관성 유지 HW NCSoft 근무 −Alterlife 프로그램 팀장 −Project M(현 Blade & Soul) 프로그램 팀장 −CTO 직속 게임기술연구팀 현 : 한국산업기술대학교 게임공학과 부교수 −학부 강의 : 게임서버프로그래밍, 멀티코어프로그래밍 −대학원 강의 : 멀티코어프로그래밍, 심화 게임서버 프로그래밍 2-2
  • 3.
    참고 삼성 첨단기술연수소와CJ E&M에서 강의중인 내용 반영 −40시간 강의 (실습 포함) => 뒷부분 만 발췌 대학원 4주 강의 분량의 압축 2-3
  • 4.
    2-4 목차 도입: 그래도 멀티쓰레드 프로그램을 하시려구요? 대책 성능 미래 또는 현실
  • 5.
    도입 제가 하는발표 들어본 적이 있으신 분? 멀티쓰레드 프로그래밍 경험 있으신 분? Lock-free 자료 구조가 무엇인지 아시는 분? 2-5 DEVIEW 2013 CJ E&M, NDC, KGC, 삼성, JCE
  • 6.
    도입 멀티쓰레드 프로그래밍의위험성 −“자꾸 죽는데 이유를 모르겠어요” 자매품 : “이상한 값이 나오는데 이유를 모르겠어요” −“더 느려져요” 2-6 [미] MuliThreadProgramming [mʌ́ltiθred-|proʊgrӕmɪŋ] : 1. 흑마술, 마공 2. 위력이 강대하나 다루기 어려워 잘 쓰이지 않는 기술
  • 7.
    도입 멀티쓰레드 프로그래밍의어려움 (한페이지 요약) −Data Race : 2를 5천만번 더했는데 1억이 안 나오는 경우. Lock을 사용해 해결 −성능 : 싱글 쓰레드 버전보다 더 느림 Lock 쓰지 말자 −컴파일러 : 변수를 참조했는데, 컴파일러가 무시 volatile 키워드로 해결, atomic으로 해결 −CPU : 프로그램 실행순서를 자기 마음대로 변조 asm mfence; 명령으로 해결, atomic으로 해결 −Cache : -1을 썼는데 65535가 써짐 포인터 주소 확인하기. 2-7 ABA 문제는?
  • 8.
    목차 도입 대책: Lock-free 프로그래밍이 도대체 뭐야? 성능 미래 또는 현실 2-8
  • 9.
    대책 현실의 멀티쓰레드프로그램은? −멀티 코어에서 여러 쓰레드가 동시에 실행된다. −쓰레드간의 데이터 공유 및 동기화는 안전한 Lock-free 자료구조를 통해서 이루어진다. −언리얼 3 : 디스플레이 리스트 Queue −각종 게임 서버 : Job Queue외 다수 2-9
  • 10.
    대책 성능을 위해서는Lock-free 알고리즘을 사용하여야 한다. 사용하지 않으면 −병렬성 감소 −Priority Inversion −Convoying −/* 성능이 떨어지고 랙이 발생한다 */ −/* 작년에 보여드렸어요~~ */ 2-10
  • 11.
    대책 Lock-free 알고리즘이란? −여러 개의 쓰레드에서 동시에 호출했을 때에도 정해진 단위 시간마다 적어도 한 개의 호출이 완료되는 알고리즘. ??????
  • 12.
    대책 Lock-free 알고리즘이란? −자료구조 및 그것에 대한 접근 방법 예) QUEUE : enqueue, dequeue 예) STACK : push, pop 예) 이진 트리 : insert, delete, search 2-12
  • 13.
    대책 Lock-free 알고리즘이란? −멀티쓰레드에서 동시에 호출해도 정확한 결과를 만들어 주는 알고리즘 STL 탈락. −Non-Blocking 알고리즘 다른 쓰레드가 어떤 상태에 있건 상관없이 호출이 완료된다. Lock 사용 시 탈락. −다른 쓰레드와 충돌하였을 경우에도 언제나 적어도 하나의 승자가 있어서, 승자는 delay없이 완료됨.
  • 14.
    대책 (보너스) −Wait-free알고리즘은? 호출이 다른 쓰레드와 충돌해도 모두 delay없이 완료 된다. 추가 상식 −LOCK을 사용하지 않는다고 lock-free 알고리즘이 아니다!!! −LOCK을 사용하면 무조건 lock-free알고리즘이 아니다.
  • 15.
    대책 알고리즘의 분류 2-15 알고리즘 싱글쓰레드 멀티쓰레드 Blocking Non-blocking Lock-free Wait-free …. ….
  • 16.
    대책 예) Blocking알고리즘 2-16 mylock.lock(); sum = sum + 2; mylock.unlock(); BLK_QUEUE::push(int x) { Node *e = New_Node(x); qlock.lock(); tail->next = e; tail = e; glock.unlock(); } while (dataReady == false); _asm mfence; temp = g_data;
  • 17.
    대책 왜 Blocking인가? −dataReady에 true가 들어가지 않으면 이 알고리즘은 무한 대기, 즉 다른 쓰레드에서 무언가 해주기를 기다린다. −여러 가지 이유로 dataReady에 true가 들어오는 것이 지연될 수 있다. Schedule out, 다른 쓰레드 때문에 대기 2-17 while (dataReady == false); _asm mfence; temp = g_data;
  • 18.
    대책 Non-blocking은? 2-18 _asm lock add sum, 2; LF_QUEUE::push(int x) { Node *e = new_Node(x); while (true) { Node *last = tail; Node *next = last->next; if (last != tail) continue; if (NULL == next) { if (CAS(&(last->next), NULL, e)) { CAS(&tail, last, e); return; } } else CAS(&tail, last, next); } } BLK_QUEUE::push(int x) { Node *e = New_Node(x); qlock.lock(); tail->next = e; tail = e; glock.unlock(); }
  • 19.
    대책 Non-blocking은? 2-19 if (dataReady == false) return false; _asm mfence; temp = g_data; while (dataReady == false); _asm mfence; temp = g_data;
  • 20.
    대책 CAS가 보이는데??? −Lock-free 알고리즘의 핵심 −CAS가 없이는 대부분의 non-blocking 알고리즘들을 구현할 수 없다. Queue, Stack, List… −CAS를 사용하면 모든 싱글쓰레드 알고리즘 들을 Lock-free 알고리즘으로 변환할 수 있다!!! 2-20
  • 21.
    대책 CAS −CAS(&A,old, new); −의미 : A의 값이 old면 new로 바꾸고 true를 리턴, 아니면 false를 리턴. −다른 버전의 의미 : A메모리를 다른 쓰레드가 먼저 업데이트 해서 false가 나왔다. 모든 것을 포기하라. 2-21
  • 22.
    대책 Lock-free 알고리즘은어떻게 구현되는가? 알고리즘의 동작이란? −기존의 자료구조의 구성을 다른 구성으로 변경하거나 자료구조에서 정보를 얻어내는 행위 2-22 3 Head Tail 1 9 X 3 Head Tail 1 9 X push(35); 35
  • 23.
    대책 Lock-free 알고리즘은어떻게 구현되는가? 2-23 자료구조를 변경을 시도한다. 성공했는가? 완료 no yes (time machine) 시도 전으로 되돌아간다.. ??? ???
  • 24.
    대책 Lock-free 알고리즘은어떻게 구현되는가? 앞의 알고리즘이 불가능 하므로 2-24 자료구조의 변경을 시도한다. but, 다른 쓰레드가 먼저 변경했으면 시도 취소. 성공했는가? 완료 yes no 현재의 자료구조를 파악한다.
  • 25.
    대책 2-25 자료구조의변경을 시도한다. but, 다른 쓰레드가 먼저 변경했으면 시도 취소. CAS while (true) { int old_sum = sum; if (true == CAS(&sum, old_sum, old_sum+2)) break; } mylock.lock(); sum = sum + 2; mylock.unlock(); while (CAS(&lock, 0, 1)); sum = sum + 2; lock = 0; 차이점?
  • 26.
    대책 2-26 자료구조의변경을 시도한다. but, 다른 쓰레드가 먼저 변경했으면 시도 취소. CAS LF_QUEUE::push(int x) { Node *e = New_Node(x); while (true) { Node *last = tail; Node *next = last->next; if (last != tail) continue; if (NULL != next) continue; if (CAS(&(last->next), NULL, e, &tail, last, e)) return; } } BLK_QUEUE::push(int x) { Node *e = New_Node(x); qlock.lock(); tail->next = e; tail = e; glock.unlock(); }
  • 27.
    대책 2-27 현실 LF_QUEUE::push(int x) { Node *e = New_Node(x); while (true) { Node *last = tail; Node *next = last->next; if (last != tail) continue; if (NULL != next) continue; if (CAS(&(last->next), NULL, e, &tail, last, e)) return; } } LF_QUEUE::push(int x) { Node *e = New_Node(x); while (true) { Node *last = tail; Node *next = last->next; if (last != tail) continue; if (NULL == next) { if (CAS(&(last->next), NULL, e)) { CAS(&tail, last, e); return; } } else CAS(&tail, last, next); } } 하지만 2개의 변수에 동시에 CAS를 적용할 수 는 없다!
  • 28.
    대책 … −알고리즘이많이 복잡하다. −그래서 작성시 실수하기가 쉽다. −실수를 적발하기가 어렵다. 하루에 한두 번 서버 크래시 가끔 가다가 아이템 증발 −제대로 동작하는 것이 증명된 알고리즘을 사용해야 한다. 2-28
  • 29.
    대책 Lock-Free 알고리즘의장단점 −장점 성능!! 높은 부하에도 안정적!! −단점 생산성 : 복잡한 알고리즘 신뢰성 : 정확성을 증명하는 것은 어렵다. 확장성 : 새로운 메소드를 추가하는 것이 매우 어렵다. 메모리 : 메모리 재사용의 관리가 어렵다. ABA문제 2-29
  • 30.
    대책 결론 −믿을수 있는 non-blocking container들을 사용하라. Intel TBB, Visual Studio PPL −자신을 포함한 출처가 의심스러운 알고리즘은 정확성을 증명하고 사용하라. 정확성이 증명된 논문에 있는 알고리즘은 OK. 2-30
  • 31.
    목차 도입 대책 성능 : 데스크탑에서 동접 1만 서버를 만들어 보자. 미래 또는 현실 2-31
  • 32.
    성능 간단한 MMORPG서버를 만들어 보자 −1000x1000 world −60x60 sector −시야 30 −1000마리의 몬스터 플레이어 접근 시 자동 이동 및 공격 −이동/공격/채팅 가능 −Windows에서 IOCP로 구현 −<2013년 게임서버프로그래밍 텀프로젝트 by 임상수>
  • 33.
    성능 성능 향상을위해 −시야 처리시 검색 성능을 위해 월드를 sector로 분할하여 주위 sector만 검색 병렬 검색을 위해 tbb::concurrent_hash_map 사용 −몬스터 AI 처리를 모든 쓰레드에서 균등하게 나누어 처리하기 위해 timer와 event시스템 사용 timer queue 병렬 등록을 위해 tbb::concurrent_priority_queue를 사용 −객체 id의 재사용을 막고 메모리 재사용을 위해 객체 id와 객체 배열의 인덱스를 쌍으로 관리  <id, index>의 병렬 검색을 위해 tbb::concurrent_hash_map 사용
  • 34.
    성능 성능 측정용DummyClient −서버에게 부하를 걸 수 있도록 여러 명의 Player를 에뮬레이션 해주는 프로그램 −사람이 플레이 하는 것과 비슷하게 Player를 주기적으로 랜덤한 방향으로 이동 시킴 −한명의 유저당 하나의 소켓 연결을 하므로 서버에서는 일반 클라이언트 접속과 DummyClient접속이 서로 차이가 없음 −훌륭한 UNIT TESTER −/* 이것도 IOCP로 구현, Direct3D로 유저 분포 실시간 디스플레이 */
  • 35.
  • 36.
    성능 실행 결과 −Intel i7 920 2.67GHz 머신에서 실행 동접 8000 정도까지 −Lock-free 자료구조로 최적화 하기 전에는 동접 3000 정도가 한계.
  • 37.
    목차 도입 대책 성능 미래 또는 현실 : Transactionl 메모리, 그리고… 2-37
  • 38.
    현실 C++11 −멀티쓰레드프로그래밍 API의 표준화 −멀티 쓰레드 표준 라이브러리  <thread>, <mutex>, <atomic>, <memory> −Boost ASIO −G++ 4.8, Visual Studio 2013 아직 최적화에 문제 : shared_ptr, mutex
  • 39.
    현실 멀티쓰레드 프로그래밍도우미 (1/2) −Intel TBB 좋은 성능, 유용한 라이브러리 Concurrent 자료 구조 −Visual Studio PPL Intel TBB와 유사 −OpenMP 컴파일러 레벨에서 병렬 프로그램 API를 제공 성능과 골치 아픈 문제들은 그대로 −그 외, CK(Concurrency Kit), Noble Library
  • 40.
    현실 암담한 현실 −Blocking Algorithm 성능 저하, priority inversion, convoying Deadlock! −Non-blocking Algorithm 높은 제작/검증 난이도 어쩌라고???? Transactional Memory!
  • 41.
    현실 Transactional Memory? 2-41 Tim Sweeny at GameTech2010
  • 42.
    현실 Transactional Memory는 −아까 본 바로 그 그림을 그대로 구현한 것… 자료구조를 변경한다. 다른 쓰레드와 충돌했는가? 완료 no yes (time machine) 시도 전으로 되돌아간다.. ???
  • 43.
    현실 Transactional Memory가좋은 이유? −생산성! −싱글쓰레드 프로그램을 그대로 쓸 수 있음 하지만 멀티쓰레드에서 Lock-free하게 실행됨. LF_QUEUE::push(int x) { Node *e = new Node(x); atomic { tail->next = e; tail = e; } } LF_QUEUE::push(int x) { Node *e = New_Node(x); while (true) { Node *last = tail; Node *next = last->next; if (last != tail) continue; if (NULL == next) { if (CAS(&(last->next), NULL, e)) { CAS(&tail, last, e); return; } } else CAS(&tail, last, next); } } Transactionl Memory 구현
  • 44.
    현실 Transactional Memory의사용법 −기존의 싱글쓰레드 프로그램에서 다른 쓰레드와 충돌이 일어날 수 있는 구간을 transaction으로 선언한다. 끝, 이게 전부
  • 45.
    현실 어떻게 가능한가??? 1. transaction 구간에서 읽고 쓴 메모리를 다른 쓰레드에서 접근했는지 검사한다. write는 실제 메모리에 쓰지 않고 잠시 대기 2. 다른쓰레드에서의 접근이 있었으면 모든 업데이트를 무효화 한 후 다시 시도. 3. 다른쓰레드에서의 접근이 없었다면 transaction 구간에서의 update를 실제 메모리에 update
  • 46.
    현실 그게 구현가능하다고?? 어떻게 −Software적으로 모든 공유 메모리 업데이트를 관리하면 됨 (STM: Software Transactional Memory) −또는, 하드웨어가 알아서 해줌 (HTM: Hardware Transactional Memory) 2-46
  • 47.
    현실 Transactional Memory 장점 −높은 생산성 기존의 프로그램을 그대로 사용 가능 단점 −STM : 오버헤드로 인한 속도 저하 −HTM : HW 필요. 한계점 −쓰레드가 너무 많을 경우 잦은 충돌로 인한 성능향상의 한계 2-47
  • 48.
    현실 Transactional Memory 지금까지는 꿈속의 개념이고 8 Core이상에서 STM의 성능을 기대하는 정도였으나. Intel에서 일을 저지름. 2-48 2013년 6월 HASWELL말매 Haswell은 HTM을 지원.
  • 49.
  • 50.
    트랜잭션 메모리의 구현 Haswell의 HTM −복수개의 메모리에 대한 Transaction을 허용한다. L1 Data Cache의 크기 만큼 −CPU에서 transaction 실패시의 복구를 제공한다. 메모리와 레지스터의 변경을 모두 Roll-back한다. −Visual Studio 2012, update 2에서 지원 2-50
  • 51.
    트랜잭션 메모리의 구현 하드웨어 트랜잭션 메모리 예제 2-51 DWORD WINAPI ThreadFunc(LPVOID lpVoid) { for (int i=0;i<500000000 / num_thread;++i) { while (_xbegin() != _XBEGIN_STARTED) _xabort(0); sum += 2; _xend(); } return 0; }
  • 52.
    트랜잭션 메모리의 구현 하드웨어 트랜잭션 메모리 예제 (Set의 Add) 2-52 bool Add(int key) { NODE *pred, *curr; NODE *node = new NODE(key); while(true) { pred = &head; curr = pred->next; while (curr->key < key) { pred = curr; curr = curr->next; } if (_XBEGIN_STARTED != my_xbegin()) { _xabort(0); continue; } if (!validate(pred, curr)) { _xabort(0); continue; } if (key == curr->key) { _xend(); delete node; return false; } else { node->next = curr; pred->next = node; _xend(); return true; } } } HTM Lock-Free
  • 53.
    트랜잭션 메모리의 구현 Haswell HTM의 한계 −모든 알고리즘에 적용 불가능 HW 용량 한계 => 알고리즘의 맞춤형 수정 필요. Nested Transaction 불가능 => 가능은 한데… −오버헤드 모든 레지스터 내용 저장 및 Roll-back 2-53 그리고…
  • 54.
  • 55.
  • 56.
    미래 HTM이 업그레이드되어서 보급되면 끝인가? −쓰레드가 많아 질 수록 충돌확률이 올라가 TM의 성능이 떨어진다. −64Core 정도가 한계일 것이라고 예측하고 있다. (2010 GameTech, Tim Sweeny)
  • 57.
    미래 왜 쓰레드사이에충돌이 생기는가? −C 스타일 언어를 사용하기 때문이다. 공유 메모리 side effect 해결책은? C 비슷한 언어를 버린다. −대신 함수형, 논리형 언어 사용 공유 메모리 없고 side effect없음
  • 58.
    새로운 언어 주목받고있는 언어 −하스켈 순수 함수형 언어로 1990년에 개발 개념은 뛰어나나 난이도로 인해 많이 사용되지 못하고 있음. −Earlang 에릭슨에서 전자 계산기용으로 1982년에 개발 Scalable한 서버 시스템에 자주 사용되고 있음 −Go, RUST 2010GDC
  • 59.
    새로운 언어 함수형언어의 프로그래밍 스타일 −모든 변수가 불변이다. (C++의 const) 불변 : 한번 값이 정해지면 바뀌지 않음. −불변 변수(immutable variable)는 data race를 일으키지 않는다.
  • 60.
    새로운 언어 하스켈 −순수 함수형 언어로 1990년에 개발 −개념은 뛰어나나 난이도로 인해 많이 사용되지 못하고 있음. −병렬성 순수 병렬성 (Parallelism) : 언어에 내재된 병렬성 이용, 항상 같은 결과값, data race나 deadlock이 전혀 없음, I/O처리 안됨 동시 실행성 (Concurrency) : I/O 처리를 위해 사용. I/O 실행순서 제어는 프로그래머가 해줘야 함. data race나 deadlock이 가능. −성능 문제 : Lazy Evaluation, Fine Grained Execution
  • 61.
    새로운 언어 Erlang −에릭슨에서 전자 교환기용으로 1982년에 개발 −Scalable한 서버 시스템에 자주 사용되고 있음 2010GDC
  • 62.
    새로운 언어 Erlang(사용예)(from wikipedia) −Amazon.com : to implement SimpleDB, providing database services as a part of the Amazon Web Services offering −Facebook : to power the backend of its chat service, handling more than 100 million active users −WhatsApp : to run messaging servers, achieving up to 2 million connected users per server. −GitHub : used for RPC proxies to ruby processes −CouchDB, Couchbase Server, Mnesia, Riak, SimpleDB
  • 63.
    새로운 언어 Erlang −process 단위의 병렬성 OS process(x), thread(x), SW context(o) 수천만개의 process 동시 실행 가능 −2006년부터 SMP(multicore) 지원 −process사이의 동기화 shared-nothing asynchronous message passing으로 구현. queue통한 message passing으로 동기화 –분산 처리 가능
  • 64.
    새로운 언어 Erlang: 분산 I/O 구현 예 % Create a process and invoke the function % web:start_server(Port, MaxConnections) ServerProcess = spawn(web, start_server, [Port, MaxConnections]), % Create a remote process and invoke the function % web:start_server(Port, MaxConnections) on machine RemoteNode RemoteProcess = spawn(RemoteNode, web, start_server, [Port, MaxConnections]), % Send a message to ServerProcess (asynchronously). % The message consists of a tuple with the atom "pause" and the number "10". ServerProcess ! {pause, 10}, % Receive messages sent to this process receive a_message -> do_something; {data, DataContent} -> handle(DataContent); {hello, Text} -> io:format("Got hello message: ~s", [Text]); {goodbye, Text} -> io:format("Got goodbye message: ~s", [Text]) end. http://en.wikipedia.org/wiki/Erlang_(programming_language)
  • 65.
    새로운 언어 함수형언어의 문제 −생산성 전혀 다른 스타일의 프로그래밍 −성능 구조체를 함수에 전달할 때 포인터가 아니라 내용을 전달하는 것과 비슷한 오버헤드. 너무 자잘한 병렬화 −IO 문제 IO는 순서대로 행해져야 하는데 함수형 언어에서 IO Operation의 순서를 정해주는 것이 비효율적이다.
  • 66.
    정리 Lock-free 알고리즘이무엇인가? −어떤 조건을 만족해야 하는가? −어떻게 구현되는가? −왜 어려운가? −하지만 왜 써야만 하는가. 멀티쓰레드 프로그래밍의 미래. −Transactional Memory 진격의 INTEL −새로운 언어의 필요 2-66
  • 67.
    NEXT 다음 주제(내년???) −Lock-free search : SKIP-LIST −ABA Problem, aka 효율적인 reference counting −고성능 MMO서버를 위한 non-blocking 자료구조의 활용 2-67
  • 68.
    Q&A 연락처 −[email protected] −발표자료 : ftp://210.93.61.41 id:ndc21 passwd: <바람의나라> −Slideshare에 올릴 예정. 참고자료 −Herlihy, Shavit, “The Art of Multiprocesor Programming, Revised”, Morgan Kaufman, 2012 2-68