View
10.433
Download
3
Category
Preview:
DESCRIPTION
Easy to understand review of nature of the Lock-free algorithm. 작년에 KGC2013에서 했던 내용의 업데이트 버전임. C++11에 맞추어 업데이트 되었음.
Citation preview
시즌 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
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
− /* 성능이 떨어지고 랙이 발생한다 */
− /* 작년에 보여드렸어요~~ */
10
대책
Lock-free 알고리즘이란?
− 여러 개의 쓰레드에서 동시에 호출했을 때에도
정해진 단위 시간마다 적어도 한 개의 호출이
완료되는 알고리즘.
11
??????
대책
Lock-free 알고리즘이란?
− 자료구조 및 그것에 대한 접근 방법
예) QUEUE : enqueue, dequeue
예) STACK : push, pop
예) 이진 트리 : insert, delete, search
12
대책
Lock-free 알고리즘이란?
− 멀티쓰레드에서 동시에 호출해도 정확한 결과를
만들어 주는 알고리즘
STL 탈락.
− Non-Blocking 알고리즘
다른 쓰레드가 어떤 상태에 있건 상관없이 호출이 완료된다.
− 호출이 다른 쓰레드와 충돌하였을 경우 적어도 하나의
승자가 있어서, 승자는 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(); mylock.lock();
q.push(35);
mylock.unlock();
while (dataReady == false);
_asm mfence;
temp = g_data;
대책
왜 Blocking인가?
− dataReady에 true가 들어가지 않으면 이 알고리즘은
무한 대기, 즉 다른 쓰레드에서 무언가 해주기를
기다린다.
− 여러 가지 이유로 dataReady에 true가 들어오는 것이
지연될 수 있다.
Schedule out, 다른 쓰레드 때문에 대기
2-17
while (dataReady == false);
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);
} }
mylock.lock();
q.push(x);
Mylock.unlock();
대책
Non-blocking은?
2-19
if (dataReady == false) return false;
_asm mfence;
temp = g_data;
대책
CAS?
− Lock-free 알고리즘의 핵심
− CAS가 없이는 대부분의 non-blocking
알고리즘들을 구현할 수 없다.
Queue, Stack, List…
− CAS를 사용하면 모든 싱글쓰레드 알고리즘
들을 Lock-free 알고리즘으로 변환할 수 있다!!!
2-20
대책
정리
− Lock-free 알고리즘을 써야 한다.
성능 때문이다.
CAS가 꼭 필요하다.
CAS − CAS(&A, old, new);
− 의미 : A의 값이 old면 new로 바꾸고 true를 리턴
− 다른 버전의 의미 : 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 알고리즘은 어떻게 구현되는가?
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();
대책 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;
}
}
QUEUE::push(int x) {
Node *e = new Node(x);
tail->next = e;
tail = e;
}
대책 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를 제공
성능과 골치 아픈 문제들은 그대로
현실
멀티쓰레드 프로그래밍 도우미 (2/2)
− CUDA, OpenCL, DirectCompute
멀티코어 활용이 아니라 GPU활용
렌더링 하느라 바쁜 GPU를 건드리지 마세요.
I/O처리가 불가능해서 서버 Core Logic에는 적용
불가능
− 그 외, CK(Concurrency Kit), Noble Library…
현실
암담한 현실
− Blocking Algorithm
성능 저하, priority inversion, convoying
Deadlock!
− Non-blocking Algorithm
높은 난이도로 인한 생산성 저하
어쩌라고????
Transactional Memory!
현실
Transactional Memory?
2-42
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-47
현실
Transactional Memory
장점
− 높은 생산성
기존의 프로그램을 그대로 사용 가능
단점
− STM : 오버헤드로 인한 속도 저하
− HTM : HW 필요.
한계점
− 쓰레드가 너무 많을 경우 잦은 충돌로 인한 성능향상의 한계
2-48
현실
Transactional Memory
지금까지는 꿈속의 개념이고 8 Core이상에서
STM의 성능을 기대하는 정도였으나.
Intel에서 일을 저지름.
2-49
2013년 6월 HASWELL말매
Haswell은 HTM을 지원.
현실
Intel Haswell
2-50
트랜잭션 메모리의 구현
Haswell의 HTM
− 복수개의 메모리에 대한 Transaction을
허용한다.
L1 Data Cache의 크기 만큼
− CPU에서 transaction 실패시의 복구를
제공한다.
메모리와 레지스터의 변경을 모두 Roll-back한다.
− Visual Studio 2012, update 2에서 지원
2-51
트랜잭션 메모리의 구현
하드웨어 트랜잭션 메모리 예제
2-52
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-53
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-54
그리고…
트랜잭션 메모리의 구현 2-55
휴…
밥줄 끊길뻔…
미래
HTM이 업그레이드 되어서 보급되면
끝인가?
− 쓰레드가 많아 질 수록 충돌확률이 올라가
TM의 성능이 떨어진다.
− 64Core 정도가 한계일 것이라고 예측하고
있다. (2010 GameTech, Tim Sweeny)
미래
왜 쓰레드사이에 충돌이 생기는가?
− C 스타일 언어를 사용하기 때문이다.
공유 메모리
side effect
해결책은? C 비슷한 언어를 버린다.
− 대신 함수형, 논리형 언어 사용
공유 메모리 없고 side effect없음
새로운 언어
주목받고 있는 언어
− 하스켈
순수 함수형 언어로 1990년에 개발
개념은 뛰어나나 난이도로 인해
많이 사용되지 못하고 있음.
− Earlang
에릭슨에서 전자 계산기용으로
1982년에 개발
Scalable한 서버 시스템에 자주
사용되고 있음
− Go, RUST
2010GDC
새로운 언어
함수형 언어의 문제
− 익히기 어렵다.
정리
Lock-free 알고리즘이 무엇인가?
− 어떤 조건을 만족해야 하는가?
− 어떻게 구현되는가?
− 왜 어려운가?
− 하지만 왜 써야만 하는가.
멀티쓰레드 프로그래밍의 미래.
− Transactional Memory
진격의 INTEL
− 새로운 언어의 필요
2-60
NEXT
다음 주제(내년???)
− Lock-free search : SKIP-LIST
− ABA Problem, aka 효율적인 reference
counting
− 고성능 MMO서버를 위한 non-blocking
자료구조의 활용
2-61
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-62
Recommended