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알고리즘이 아니다.
17. 대책
왜 Blocking인가?
−dataReady에 true가 들어가지 않으면 이 알고리즘은 무한 대기, 즉 다른 쓰레드에서 무언가 해주기를 기다린다.
−여러 가지 이유로 dataReady에 true가 들어오는 것이 지연될 수 있다.
Schedule out, 다른 쓰레드 때문에 대기
2-17
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로 유저 분포 실시간 디스플레이 */
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!
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을 지원.
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; }
53. 트랜잭션 메모리의 구현
Haswell HTM의 한계
−모든 알고리즘에 적용 불가능
HW 용량 한계 => 알고리즘의 맞춤형 수정 필요.
Nested Transaction 불가능 => 가능은 한데…
−오버헤드
모든 레지스터 내용 저장 및 Roll-back
2-53
그리고…
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
연락처
−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