242 naver-2

104
멀티쓰레드 프로그래밍이 왜이리 힘드나요? (컴파일러와 하드웨어에서 Lock-free 알고리즘 까지) 정내훈 한국산업기술대학교 게임공학과

Transcript of 242 naver-2

Page 1: 242 naver-2

멀티쓰레드 프로그래밍이

왜이리 힘드나요?

(컴파일러와 하드웨어에서 Lock-free

알고리즘 까지)

정내훈

한국산업기술대학교 게임공학과

Page 2: 242 naver-2

발표자 소개

• KAIST 전산과 박사

– 전공 : 멀티프로세서 CPU용 일관성 유지 HW

• NCSoft 근무

– Alterlife 프로그램 팀장

– Project M 프로그램 팀장

– CTO 직속 게임기술연구팀

• 현 : 한국산업기술대학교 게임공학과 부교수

– 학부 강의 : 게임서버프로그래밍

– 대학원 강의 : 멀티코어프로그래밍, 심화 게임서버

프로그래밍

2-2

Page 3: 242 naver-2

참고

• NDC2012, KGC2012, CJE&M에서 강연한

내용

– 업데이트

• 삼성첨단기술연수소에서 강의한 내용

– 40시간 강의 (실습 포함) 의 앞부분

• 대학원 처음 4주 강의 분량의 압축

2-3

Page 4: 242 naver-2

2-4

목차

• 도입

• 현실

• Welcome to the Hell.

• 새로운 희망

• 우리의 나아갈 길

Page 5: 242 naver-2

도입

• 멀티쓰레드 프로그래밍 이란?

– 멀티코어 혹은 멀티프로세서 컴퓨터의 성능을

이끌어 내기 위한 프로그래밍 기법

– 흑마술의 일종

• 잘못 사용하면 패가 망신

2-5

Page 6: 242 naver-2

도입

• 흑마술 멀티쓰레드 프로그래밍의 위험성

– “자꾸 죽는데 이유를 모르겠어요”

• 자매품 : “이상한 값이 나오는데 이유를 모르겠어요”

– “더 느려져요”

2-6

[미] MuliThreadProgramming [mʌ́ltiθred-|proʊgrӕmɪŋ] : 1. 흑마술, 마공

2. 위력이 강대하나 다루기 어려워 잘 쓰이지 않는 기술

Page 7: 242 naver-2

2-7

내용

• 도입

• 현실

• Welcome to the Hell.

• 새로운 희망

• 우리의 나아갈 길

Page 8: 242 naver-2

현실

• “멀티쓰레드 안 해도 되지 않나요?”

– NO!

– “MultiThread 프로그래밍을 하지 않는 이상

프로그램의 성능은 전혀 나아지지 않을 것임” –

by Intel, AMD

• “공짜 점심은 끝났어요~~”

2-8

Page 9: 242 naver-2

현실

2-9

피할 곳도 숨을 곳도 없습니다.

Page 10: 242 naver-2

현실

• 멀티쓰레드 프로그래밍을 하지 않으면?

– (멀티코어) CPU가 놀아요.

– 경쟁회사 제품보다 느려요.

• FPS(Frames Per Second)

• 동접

– 점점 줄어드는 사용자당 수입

– 만일 중국에 출동하면??

2-10

Page 11: 242 naver-2

현실

• 멀티 코어 CPU가 왜 나왔는가?

– 예전에는 만들기 힘들어서? No

– 다른 방법들의 약발이 다 떨어져서!

• 클럭 속도, 캐시, 슈퍼스칼라, Out-of-order, 동적 분기

예측…

– 늦게 나온 이유

• 프로그래머에게 욕을 먹을 것이 뻔하기 때문.

– 기존 프로그램의 성능향상이 전혀 없고, 멀티 쓰레드

프로그래밍이 너무 어려워서.

2-11

Page 12: 242 naver-2

현실

• 컴퓨터 공학을 전공했지만 학부에서

가르치지 않았다.

• 큰맘 먹고 스터디를 시작했지만 한 달도 못

가서 흐지부지 되었다. (원인은 다음 페이지)

• 그냥 멀티쓰레드 안 쓰기로 했다.

2-12

Page 13: 242 naver-2

현실

• 좋은 교재

2-13

Page 14: 242 naver-2

현실

• 왜 멀티쓰레드 프로그래밍이 어려운가?

– 다른 쓰레드의 영향을 고려해서 프로그램 해야 하기

때문에

– 에러 재현과 디버깅이 힘들어서

– Visual Studio가 사기를 치고 있기 때문

• 왜 멀티쓰레드 프로그래밍이 진짜로 어려운가?

– CPU가 사기를 치고 있기 때문

2-14

Page 15: 242 naver-2

2-15

내용

• 도입

• 현실

• Welcome to the Hell.

• 새로운 희망

• 우리의 나아갈 길

Page 16: 242 naver-2

고생길

• Visual Studio의 사기

– 참조 <simple_sync>

2-16

DWORD WINAPI ThreadFunc1(LPVOID lpVoid)

{

data = 1;

flag = true;

}

DWORD WINAPI ThreadFunc2(LPVOID lpVoid)

{

while(!flag);

my_data = data;

}

Page 17: 242 naver-2

고생길

• Visual Studio의 사기

– 참조 <simple_sync>

2-17

DWORD WINAPI ThreadFunc2(LPVOID lpVoid)

{

while(!flag);

my_data = data;

}

DWORD WINAPI ThreadFunc2(LPVOID lpVOid)

{

00951020 mov al,byte ptr [flag (953374h)]

while (!flag);

00951025 test al,al

00951027 je ThreadFunc2+5 (951025h)

int my_data = data;

printf("Data is %X\n", my_data);

00951029 mov eax,dword ptr [data (953370h)]

0095102E push eax

0095102F push offset string "Data is %X\n"

(952104h)

00951034 call dword ptr [__imp__printf

(9520ACh)]

0095103A add esp,8

return 0;

0095103D xor eax,eax

}

0095103F ret 4

싱글 쓰레드 프로그램이면?

VS는 무죄!

Page 18: 242 naver-2

고생길

• Visual Studio의 사기를 피하는 방법

– volatile을 사용하면 된다.

• 최적화를 하지 않는다.

• 반드시 메모리를 읽고 쓴다.

• 읽고 쓰는 순서를 지킨다.

– 참 쉽죠?

– “어셈블리를 모르면 Visual Studio의 사기를 알

수 없다” 흠좀무…

2-18

Page 19: 242 naver-2

고생길

• 정말 쉬운가???

2-19

struct Qnode {

volatile int data;

volatile Qnode* next;

};

DWORD WINAPI ThreadFunc1(LPVOID lpVoid)

{

while ( qnode->next == NULL ) { }

my_data = qnode->next->data;

}

무엇이 문제일까??

Page 20: 242 naver-2

고생길

• volatile의 사용법

– volatile int * a;

• *a = 1; // 순서를 지킴

• a = b; // 순서를 지키지 않는다.

– int * volatile a;

• *a = 1; // 순서를 지키지 않음,

• a = b; // 이것은 순서를 지킴

Page 21: 242 naver-2

고생길

• Volatile 위치 오류의 예

Qnode * volatile next; volatile Qnode* next;

01191090 mov eax,dword ptr [esi+4]

01191093 test eax,eax

01191095 je ThreadFunc+90h (1191090h)

011F1089 mov eax,dword ptr [esi+4]

011F108C lea esp,[esp]

011F1090 cmp eax,ebx

011F1092 je ThreadFunc+90h (11F1090h)

void UnLock() {

Qnode *qnode;

qnode = myNode;

if (qnode->next == NULL) {

LONG long_qnode = reinterpret_cast<LONG>(qnode);

volatile LONG *long_tail = reinterpret_cast<volatile LONG*>(&tail);

if ( CAS(long_tail, NULL, long_qnode) ) return;

while ( qnode->next == NULL ) { }

}

qnode->next->locked = false;

qnode->next = NULL;

}

Page 22: 242 naver-2

고생길

• Thread 2개로 합계 1억을 만드는 프로그램

2-22

#include <windows.h>

#include <stdio.h>

volatile int sum = 0;

DWORD WINAPI ThreadFunc(LPVOID lpVoid)

{

for (int i=1;i<=25000000;i++) sum += 2;

return 0;

}

int main()

{

DWORD addr;

HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &addr);

HANDLE hThread3 = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &addr);

WaitForSingleObject(hThread2, INFINITE);

WaitForSingleObject(hThread3, INFINITE);

CloseHandle(hThread2);

CloseHandle(hThread3);

printf(“Result is %d\n", sum);

getchar();

return 0;

}

Page 23: 242 naver-2

고생길

• Thread 2개로 합계 1억을 만드는 프로그램의 최신

유행

2-23

Page 24: 242 naver-2

고생길

• 결과는?

• 엉뚱한 답

2-24

Page 25: 242 naver-2

고생길

• 다중 쓰레드 - 결과

2-25

Page 26: 242 naver-2

고생길

• 왜 틀린 결과가 나왔을까?

─ “sum+=2”가 문제이다.

2-26

Page 27: 242 naver-2

고생길

• 왜 틀린 결과가 나왔을까?

– DATA RACE (복수의 쓰레드에서 같은 공유

메모리에 WRITE하는 행위) 때문.

– “sum+=2”가 문제이다.

2-27

쓰레드 1 쓰레드 2

MOV EAX, SUM

ADD EAX, 2

MOV SUM, EAX

MOV EAX, SUM

ADD EAX, 2

MOV SUM, EAX

sum = 200

sum = 200

sum = 202

sum = 202

Page 28: 242 naver-2

고생길

• 하지만

• 이 출동하면?

• 왜????

– 대학 2학년 때 컴퓨터구조와 운영체제시간에

배움

2-28

ADD SUM, 2

Page 29: 242 naver-2

고생길

• Data Race의 해결 방법은?

– Data Race를 없애면 된다.

• 어떻게

– Lock과 Unlock을 사용한다.

– Windows에서는 EnterCriticalSection(),

LeaveCriticalSection()

– Linux에서는 pthread_mutex_lock(),

pthread_mutex_un

– C++11에서는 std::mutex의 lock(), unlock()

2-29

Page 30: 242 naver-2

고생길

• 결과

2-30

Page 31: 242 naver-2

고생길

• 결과가 옳게 나왔다. 만족하는가?

2-31

실행시간 결과

1 Thread 280,577 100000000

2 Thread 146,823 50876664

4 Thread 132,362 27366758

실행시간 결과

1 Thread 2,888,071 100000000

2 Thread 5,947,291 100000000

4 Thread 4,606,754 100000000

No LOCK

With LOCK 35배의 성능차이

Page 32: 242 naver-2

고생길

• EnterCriticalSection() 이라는 물건은

– 한번에 하나의 쓰레드만 실행 시킴

– Lock을 얻지 못하면 시스템 호출

2-32

Page 33: 242 naver-2

고생길

• 해결 방법은?

– Lock을 쓰지 않으면 된다.

– “Sum += 2”를 Atomic하게 만들면 된다.

• Atomic

– 실행 중 다른 Core가 끼어들지 못하도록 한다.

2-33

Page 34: 242 naver-2

고생길

• 결과가 옳게 나왔다. 만족하는가?

2-34

실행시간 결과

1 Thread 280,577 100000000

2 Thread 146,823 50876664

4 Thread 132,362 27366758 실행시간 결과

1 Thread 2,888,071 100000000

2 Thread 5,947,291 100000000

4 Thread 4,606,754 100000000

No LOCK

With LOCK

실행시간 결과

1 Thread 1,001,528 100000000

2 Thread 1,462,121 100000000

4 Thread 1,452,311 100000000

With InterlockedOperation

Page 35: 242 naver-2

고생길

• 정답은?

2-35

Page 36: 242 naver-2

고생길

• 만족하는가? (i7 – 4core)

2-36

실행시간 결과

1 Thread 280,577 100000000

2 Thread 146,823 50876664

4 Thread 132,362 27366758

실행시간 결과

1 Thread 2,888,071 100000000

2 Thread 5,947,291 100000000

4 Thread 4,606,754 100000000

No LOCK With LOCK

실행시간 결과

1 Thread 1,001,528 100000000

2 Thread 1,462,121 100000000

4 Thread 1,452,311 100000000

With InterlockedOperation

실행시간 결과

1 Thread 287,776 100000000

2 Thread 156,394 100000000

4 Thread 96,925 100000000

정답

Page 37: 242 naver-2

고생길

• 만족하는가? (XEON E5405, 2CPU)

2-37

실행시간 결과

1 Thread 1,798 100000000

2 Thread 2,926 520251348

4 Thread 1,692 203771150

8 Thread 3,699 193307522

실행시간 결과

1 Thread 27,073 100000000

2 Thread 83,586 100000000

4 Thread 69,264 100000000

8 Thread 67,156 100000000

No LOCK With LOCK

With InterlockedOperation

실행시간 결과

1 Thread 2055 100000000

2 Thread 1798 100000000

4 Thread 834 100000000

8 Thread 419 100000000

정답

실행시간 결과

1 Thread 11,307 100000000

2 Thread 24,194 100000000

4 Thread 20,292 100000000

8 Thread 18,699 100000000

Page 38: 242 naver-2

고생길

• 만족하는가? (XEON E5-4620, 4CPU, 32Core)

2-38

실행시간

결과

1 Thread 0.425 100000000

2 Thread 0.678 56567966

4 Thread 0.768 27254540

8 Thread 1.009 16257652

16Thread 0.942 14320406

32Thread 1.706 8570996

64Thread 1.926 3855910

No LOCK

With LOCK

Interlocked

Operation 정답

실행시간

결과

1 Thread 1.703 100000000

2 Thread 13.21 100000000

4 Thread 11.45 100000000

8 Thread 32.27 100000000

16Thread 46.10 100000000

32Thread 80.76 100000000

64Thread 80.32 100000000

실행시간

결과

1 Thread 0.877 100000000

2 Thread 3.344 100000000

4 Thread 2.653 100000000

8 Thread 2.515 100000000

16Thread 2.624 100000000

32Thread 3.353 100000000

64Thread 3.061 100000000

실행시간

결과

1 Thread 0.422 100000000

2 Thread 0.328 100000000

4 Thread 0.168 100000000

8 Thread 0.080 100000000

16Thread 0.080 100000000

32Thread 0.043 100000000

64Thread 0.028 100000000

Page 39: 242 naver-2

고생길

• 지금까지

– Visual Studio의 마수에서 벗어나기

• Volatile을 잘 쓰자

– 경쟁상태 해결하기.

• Lock을 최소화 하자

• Lock대신 atomic operation을 사용하자

2-39

Page 40: 242 naver-2

고생길

• 그러나

– 절대로 모든 문제가 정답처럼 풀리지 않는다.

– Interlocked로 구현 가능하면 다행 (atomic)

• Interlock이 가능한 것은 일부 Instruction

– 일반적인 자료구조를 Lock없이 Atomic하게

구현하는 것은 큰 문제다.

– Lock말고도 다른 문제가 있다.

2-40

Page 41: 242 naver-2

HELL

• 멀티 코어에서는 Data Race말고도 다른

문제점이 있다.

• “상상한 것 그 이상을 보여준다”, 충공깽

2-41

Page 42: 242 naver-2

HELL

2-42

EnterCriticalSection()이 문제가 있으니 나만의 Lock을 구현해 볼까?

Page 43: 242 naver-2

HELL

• 다음의 프로그램으로 Lock과 Unlock이 동작할까?

– 피터슨 알고리즘

– 두 개의 쓰레드에서 Lock구현

– 운영체제 교과서에 실려 있음

– threadId는 0과 1이라고 가정 (myID에 저장)

• 실행해 보자

• 결과는?

2-43

volatile int victim = 0;

volatile bool flag[2] = {false, false};

Lock(int myID)

{

int other = 1 – myID;

flag[myID] = true;

victim = myID;

while(flag[other] && victim == myID) {}

}

Unlock (int myID)

{

flag[myID] = false;

}

Page 44: 242 naver-2

HELL

• 결과는?

2-44

Page 45: 242 naver-2

HELL

• 이유는?

– CPU는 사기를 친다.

• Line Based Cache Sharing

• Out of order execution

• write buffering

– CPU는 프로그램을 순차적으로 실행하는

척만한다.

• 자기 자신이 실행하는 프로그램에게는 제대로

실행하는 것처럼 거짓말한다.

• 옆의 Core에서 보면 거짓말이 보인다.

2-45

Page 46: 242 naver-2

HELL

• Out-of-order 실행

2-46

a = fsin(b);

f = 3;

a = b; // a,b는 cache miss

c = d; // c,d는 cache hit

Page 47: 242 naver-2

HELL

• 문제는 메모리

– 프로그램 순서대로 읽고 쓰지 않는다.

• 읽기와 쓰기는 시간이 많이 걸리므로.

• 옆의 프로세서(core)에서 보면 속도차와 실행순서

뒤바뀜이 보인다.

• 어떠한 일이 벌어지는가?

2-47

Page 48: 242 naver-2

병행성과 정확성

• 아래의 두 개의 실행결과는 서로 다르다

어떠한 것이 정확한 결과인가?

2-48

write (x, 1)

read(x, 2)

write(x, 2)

read(x, 2)

thread a thread b Type-A

Type-B !! write (x, 1)

write (y, 1)

read(y, 1)

read(x, 0)

thread a thread b

Page 49: 242 naver-2

병행성과 정확성

• 그러면 이것은?

2-49

write (x, 1)

read(x, 2)

write(x, 2)

read(x, 1)

thread a thread b

Type-C!!

Type-D!! write (x, 1)

read (y, 0)

write(y, 1)

Read(x, 0)

thread a thread b

Page 50: 242 naver-2

HELL

• 현실

– 앞의 여러 형태의 결과는 전부 가능하다.

• 부정확해 보이는 결과가 나오는 이유?

– 현재의 CPU는 Out-of-order실행을 한다.

– 메모리의 접근은 순간적이 아니다.

– 멀티 코어에서는 옆의 코어의 Out-of-order

실행이 관측된다.

2-50

Page 51: 242 naver-2

HELL

• 진짜?

• 확인해 보자.

• 메모리 접근 순서를 강제로 맞추어 주는

명령어

• 앞에 피터슨 알고리즘에 적용해보자.

– 근데… 오류의 확률이 낮아서…

2-51

_asm mfence;

Page 52: 242 naver-2

HELL

• 메모리 접근 오류 검출 프로그램을

사용해보자

• 아이디어.

– 메모리 내용을 계속 업데이트 하면서 다른

쓰레드의 업데이트를 같이 기록하여 나중에

기록된 로그를 비교해 보자.

2-52

Page 53: 242 naver-2

HELL

• 정말 간단한 프로그램

2-53

#define THREAD_MAX 2

#define SIZE 10000000

volatile int x,y;

int trace_x[SIZE], trace_y[SIZE];

DWORD WINAPI ThreadFunc0(LPVOID a)

{

for(int i = 0; i <SIZE;i++) {

x = i;

trace_y[i] = y;

}

return 0;

}

DWORD WINAPI ThreadFunc1(LPVOID a)

{

for(int i = 0; i <SIZE;i++) {

y = i;

trace_x[i] = x;

}

return 0;

}

int main()

{

DWORD addr;

HANDLE hThread[THREAD_MAX];

.. // Thread 2개 실행

int count = 0;

for (int i=0; i< SIZE;++i)

if (trace_x[i] == trace_x[i+1])

if (trace_y[trace_x[i]] == trace_y[trace_x[i] + 1]) {

if (trace_y[trace_x[i]] != i) continue;

count++;

}

printf("Total Memory Inconsistency:%d\n", count);

return 0;

}

Page 54: 242 naver-2

HELL

• 프로그램 설명

2-54

7

7

8

8

8

9

1

2

2

3

5

6

2

3

4

5

6

7

6

7

8

9

10

11

x traceY y traceX

8보다 3이 먼저 write!!

3보다 8이 먼저 write!!!

Page 55: 242 naver-2

HELL

2-55

공황상태…

Page 56: 242 naver-2

HELL

• 메모리 변경 순서가 뒤바뀔 확률은?

• _asm mfence를 넣어보자.

– 또는 C++11에서

2-56

#include <atomic>

std::atomic_thread_fence(std::memory_order_seq_cst);

Page 57: 242 naver-2

병행성과 정확성

• 메모리에는 유령이

2-57

volatile bool done = false;

volatile int *bound;

int error;

DWORD WINAPI ThreadFunc1(LPVOID lpVoid)

{

for (int j = 0; j<= 25000000; ++j) *bound = -(1 + *bound);

done = true;

return 0;

}

DWORD WINAPI ThreadFunc2(LPVOID lpVOid)

{

while (!done) {

int v = *bound;

if ((v !=0) && (v != -1)) error ++;

}

return 0;

}

Page 58: 242 naver-2

병행성과 정확성

• 어떻게 실행했길래?

2-58

int ARR[32];

int temp = (int) &ARR[16];

temp = temp & 0xFFFFFFC0;

temp -= 2;

bound = (int *) temp;

*bound = 0;

HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunc1, (LPVOID) 0, 0, &addr);

HANDLE hThread3 = CreateThread(NULL, 0, ThreadFunc2, (LPVOID) 1, 0, &addr);

Page 59: 242 naver-2

HELL

• 어떻게 실행했길래?

2-59

bound

2byte

2byte

Cache line

Page 60: 242 naver-2

HELL

• 결과가….

– 중간값

• write시 최종값과 초기값이 아닌 다른 값이 도중에 메모리에

써지는 현상

– 이유는?

• Cache Line Size Boundary

– 대책은?

• Pointer를 절대 믿지 마라.

• Byte 밖에 믿을 수 없다.

• Pointer가 아닌 변수는

– Visual C++ 또는 G++가 잘 해준다.

2-60

short buf[256] buf[0] = length; buf[1] = OP_MOVE; *((float *)(&buf[2])) = x; *((float *)(&buf[4])) = y; *((float *)(&buf[6])) = z; *((float *)(&buf[8])) = dx; *((float *)(&buf[10])) = dy; *((float *)(&buf[12])) = dz; *((float *)(&buf[14])) = ax; *((float *)(&buf[16])) = ay; *((float *)(&buf[18])) = az; *((int *)(&buf[20])) = h; … send( fd, buf, (size_t)buf[0], 0 );

어디서 많이 본 소스코드..

Page 61: 242 naver-2

HELL

• 이러한 현상을 메모리 일관성(Memory

Consistency) 문제라고 부른다.

– x86은 얌전한 편, ARM CPU는 더하다.

2-61

http://en.wikipedia.org/wiki/Memory_ordering

Page 62: 242 naver-2

HELL

• 정리

– 멀티쓰레드에서의 공유 메모리

• 다른 코어에서 보았을 때 업데이트 순서가 틀릴 수 있다.

• 메모리의 내용이 한 순간에 업데이트 되지 않을 때 도 있다.

– 일반적인 프로그래밍 방식으로는 멀티쓰레드에서

안정적으로 돌아가는 프로그램을 만들 수 없다.

2-62

Page 63: 242 naver-2

HELL

• 어떻게 할 것인가?

– 위의 상황을 감안하고 프로그램 작성

• 프로그래밍이 너무 어렵다.

– 피터슨이나 빵집 알고리즘도 동작하지 않는다.

– 모든 공유메모리 접근을 Atomic하도록 수정한다.

• 모든 메모리 접근을 Lock/Unlock으로 막으면 가능

– 성능저하!!!, Lock은 어떻게 구현?

• Interlocked Operation 사용

– 간단한 연산만 가능, 성능저하

• mfence의 적절한 추가

– 적절하다는 보장은???

2-63

어쩌라고???

Page 64: 242 naver-2

2-64

내용

• 도입

• 현실

• Welcome to the Hell.

• 새로운 희망

• 우리의 나아갈 길

Page 65: 242 naver-2

희망

• 언젠가는 메모리에 대한 쓰기가 실행 된다.

• 자기 자신의 프로그램 실행순서는 지켜진다.

• 캐시의 일관성은 지켜진다.

– 한번 지워졌던 값이 다시 살아나지는 않는다.

– 언젠가는 모든 코어가 동일한 값을 본다

• 캐시라인 내부의 쓰기는 중간 값을 만들지

않는다.

2-65

Page 66: 242 naver-2

희망

• 우리가 할 수 있는 것

– CPU의 여러 삽질에도 불구 하고 주의 깊게

프로그래밍 하면 모든 메모리 접근을

Atomic하게 할 수 있다.

• HW의 도움 없이도 가능.

• 하지만 mfence가 효율적

2-66

Page 67: 242 naver-2

희망

• Atomic Memory 만 있으면 되는가?

– NO

• 진짜 큰 규모의 상용 멀티쓰레드

프로그래밍은?

– 쓰레드간의 동기화나 자료 전송은 고유의

자료구조 사용

• Queue, Stack, List, Map, Tree……

• 예) Tera의 시야처리용 Lock-free job queue

• 예) Unreal3의 rendering command queue

2-67

Page 68: 242 naver-2

희망

• 하지만.

– 지금까지 배운 모든 자료구조가

멀티쓰레드에서는 동작하지 않는다.

– STL도 동작하지 않는다.

– 다시 작성해야 한다.

• LOCK을 쓰면?

2-68

“Lock 없애야 해요 Lock 없앨 때 마다

동접이 300명씩 늘어났어요.” - N모사에서

L모 게임을 만들었던 S모님

Page 69: 242 naver-2

Lock없는 프로그램

• 효율적인 구현

– Lock없는 구현

• 성능 저하의 주범이므로 당연

– Overhead & Critical Section

– Priority inversion

– Convoying

– Lock이 없다고 성능저하가 없는가??

• 상대방 쓰레드에서 어떤 일을 해주기를 기다리는 한

동시실행으로 인한 성능 개선을 얻기 힘들다.

– while (other_thread.flag == true);

– lock과 동일한 성능저하

• 상대방 쓰레드의 행동에 의존적이지 않는 구현방식이 필요하다.

2-69

Page 70: 242 naver-2

Non-Blocking

• 블럭킹 (blocking)

– 다른 쓰레드의 진행상태에 따라 진행이 막힐 수

있음

• 예) while(lock != 0);

– 멀티쓰레드의 bottle neck이 생긴다.

– Lock을 사용하면 블럭킹

• 넌블럭킹 (non-blocking)

– 다른 쓰레드가 어떠한 삽질을 하고 있던

상관없이 진행

• 예) 공유메모리 읽기/쓰기, Interlocked Operation

2-70

Page 71: 242 naver-2

Non-Blocking

• 블럭킹 알고리즘의 문제

– 성능저하

– Priority Inversion

• Lock을 공유하는 덜 중요한 작업들이 중요한 작업의 실행을

막는 현상

• Reader/Write Problem에서 많이 발생

– Convoying

• Lock을 얻은 쓰레드가 스케쥴링에서 제외된 경우, lock을

기다리는 모든 쓰레드가 공회전

• Core보다 많은 수의 thread를 생성했을 경우 자주 발생.

• 성능이 낮아도 Non-Blocking이 필요할 수 있다.

2-71

Page 72: 242 naver-2

Non-Blocking

• 넌블럭킹의 등급

– 무대기 (wait-free)

• 모든 메소드가 정해진 유한한 단계에 실행을 끝마침

• 멈춤 없는 프로그램 실행

– 무잠금 (lock-free)

• 항상, 적어도 한 개의 메소드가 유한한 단계에 실행을 끝마침

• 무대기이면 무잠금이다

• 기아(starvation)을 유발하기도 한다.

• 성능을 위해 무대기 대신 무잠금을 선택하기도 한다.

2-72

Page 73: 242 naver-2

Non-Blocking

• 정리

– Wait-free, Lock-free

• Lock을 사용하지 않고

• 다른 쓰레드가 어떠한 행동을 하기를 기다리는 것

없이

• 자료구조의 접근을 Atomic하게 해주는 알고리즘의

등급

– 멀티 쓰레드 프로그램에서 쓰레드 사이의

효율적인 자료 교환과 협업을 위해서는 Non-

Blocking 자료 구조가 필요하다.

2-73

Page 74: 242 naver-2

병행성과 정확성

• 그러면, Atomic Memory로 그런 자료구조를

만들면 되지 않는가?

• Atomic Memory만으로는 다중 쓰레드

무대기 큐를 만들 수 없다!!!!!!

– (증명) : 아까 그 책

2-74

Page 75: 242 naver-2

병행성과 정확성

• 다중 쓰레드 무대기 큐를 만들려면?

– CAS 명령어가 필요하다.

− CAS가 없이는 대부분의 non-blocking

알고리즘들을 구현할 수 없다.

• Queue, Stack, List…

− CAS를 사용하면 모든 싱글쓰레드 알고리즘

들을 Lock-free 알고리즘으로 변환할 수 있다!!!

− Lock-free 알고리즘의 핵심

2-75

Page 76: 242 naver-2

CAS

• CAS − CAS(&A, old, new);

− 의미 : 아래의 연산을 Atomic하게 수행

− 다른 버전의 의미 : A메모리를 다른 쓰레드가 먼저

업데이트 해서 false가 나왔다. 모든 것을 포기하라.

2-76

if (A == old) { A = new; return true; }

else return false;

Page 77: 242 naver-2

CAS

• 구현 : Windows

– API

– CAS의 구현

2-77

#include <windows.h>

LONG __cdecl InterlockedCompareExchange(

__inout LONG volatile *Destination,

__in LONG Exchange,

__in LONG Comparand );

Bool CAS(LONG volatile *Addr, LONG New, LONG Old)

{

LONG temp = InterlockedCompareExchange(Addr, New, Old);

return temp == Old;

}

Page 78: 242 naver-2

CAS

• 구현 : LINUX

2-78

#include <stdbool.h>

bool CAS(int *ptr, int oldval, int newval)

{

return __sync_bool_compare_and_swap(ptr, oldval, newval);

}

Page 79: 242 naver-2

CAS

• 구현 : C++11

2-79

#include <atomic>

bool atomic_compare_exchange_strong( std::atomic<T>* obj,

T* expected, T desired );

Page 80: 242 naver-2

CAS

• 실제 HW (x86 계열 CPU) 구현

– LOCK prefix와 CMPXCHG 명령어로 구현

– lock cmpxchg [A], b 기계어 명령으로

구현

• eax에 비교값, A에 주소, b에 넣을 값

2-80

if (eax == [a]) {

ZF = true;

[a] = b;

} else {

ZF = false;

eax = [a];

}

Page 81: 242 naver-2

CAS

• 실제 HW (ARM) 구현

2-81

static inline AtomicWord CompareAndSwap(volatile AtomicWord* ptr,

AtomicWord old_value,

AtomicWord new_value)

{

uint32_t old, tmp;

__asm__ __volatile__("1: @ atomic cmpxchg\n"

"mov %0, #0\n"

"ldrex %1, [%2]\n"

"teq %1, %3\n"

"strexeq %0, %4, [%2]\n"

"teq %0, #0\n"

"bne 1b"

: "=&r" (tmp), "=&r" (old)

: "r" (ptr), "Ir" (old_value),

"r" (new_value)

: "cc");

return old;

}

Page 82: 242 naver-2

CAS

• CAS의 위용

– 모든 자료구조를 멀티쓰레드 무대기자료구조로

만들 수 있다.

• 증명이 되어 있다.

– 바꿔주는 프로그램이 있다.

– STL도 OK!

2-82

Page 83: 242 naver-2

CAS

• 모든 자료구조를 멀티쓰레드 Lock-Free로 바꿔주는 프로그램

2-83

class LFUniversal {

private:

Node *head[N], Node tail;

public:

LFUniversal() {

tail.seq = 1;

for (int i=0;i<N;++i) head[i] = &tail;

}

Response apply(Invocation invoc) {

int i = Thread_id();

Node prefer = Node(invoc);

while (prefer.seq == 0) {

Node *before = tail.max(head);

Node *after = before->decideNext->decide(&prefer);

before->next = after; after->seq = before->seq + 1;

head[i] = after;

}

SeqObject myObject;

Node *current = tail.next;

while (current != &prefer) {

myObject.apply(current->invoc);

current = current->next;

}

return myObject.apply(current->invoc);

} };

Page 84: 242 naver-2

희망

• Happy End????

– NO

• 왜?

– 구현은 쉽다.

– 성능이 엉망이다.

2-84

Page 85: 242 naver-2

이론 시간

• XEON, E5-4620, 2.2GHz, 4CPU (32 core)

• STL의 queue를 무잠금, 무대기로 구현한 것과,

CriticalSection으로 atomic하게 만든 것의 성능 비교.

– Test조건 : 16384번 Enqueue, Dequeue (결과는 mili second)

– EnterCriticaSection()을 사용한 것은 테스트 데이터의 크기가 100배

– 따라서 100배 성능 차이 (4개 thread의 경우)

• 그렇다면, EnterCriticalSection을 사용해야 하는가?

– No : 멀티쓰레드에서의 성능향상이 없다.

쓰레드 갯수 1 2 4 8 16 32 64

무잠금 만능 3749 1966 1697 1120 742 525 413

무대기 만능 3640 1964 1219 1136 577 599 448

EnterCritical 232 822 1160 1765 1914 4803 7665

Page 86: 242 naver-2

희망

• 결론

– CPU가 제공하는 CAS 명령어를 사용하면

기존의 모든 싱글쓰레드 알고리즘을 Lock-

free한 멀티쓰레드 알고리즘으로 변환할 수

있다.

• 현실

– Universal Algorithm은 비효율 적이다.

2-86

Page 87: 242 naver-2

희망

• 대안

– 자료구조에 맞추어 최적화된 lock-

free알고리즘을 일일이 개발해야 한다.

• 멀티쓰레드 프로그램은 힘들다. => 연봉이 높다.

• 다른 데서 구해 쓸 수도 있다.

– Intel TBB, VS2012 PPL

– 인터넷

– 하지만 범용적일 수록 성능이 떨어진다.

자신에게 딱 맞는 것을 만드는 것이 좋다.

2-87

Page 88: 242 naver-2

2-88

내용

• 도입

• 현실

• Welcome to the Hell.

• 새로운 희망

• 우리의 나아갈 길

Page 89: 242 naver-2

Non-Blocking

• 우리의 목적

– 정확한 결과

– 고성능

• 번역하면

– Lock을 사용하지 않고

– 비멈춤 (wait-free, lock-free)

– 자료구조 (Queue, Stack, List~~~)

2-89

Page 90: 242 naver-2

Non-Blocking

• 지향하는 프로그래밍 스타일

– Lock을 사용한 프로그래밍

• Blocking

• 느림 (몇 백배)

– 원자적 레지스터를 사용한 프로그래밍

• 표현력이 떨어짐

• Queue도 만들지 못함

– Non-blocking 자료구조를 사용한 프로그래밍

• OK

2-90

Page 91: 242 naver-2

2-91

내용

• 도입

• 현실

• Welcome to the Hell.

• 새로운 희망

• 우리의 나아갈 길 => 실제 예제

Page 92: 242 naver-2

예제

• Non Blocking 자료 구조의 구현 법

• 예제 : 정렬된 링크드 리스트를 사용한 집합

– int를 집합에 add(), remove(), find()할 수 있는

자료 구조

• 성능 비교

Page 93: 242 naver-2

리스트의 구현

• 1차 구현 : Lock의 사용 bool Remove(int key)

{

NODE *pred, *curr;

pred = &head;

EnterCriticalSection(&glock);

curr = pred->next;

while (curr->key < key) {

pred = curr; curr = curr->next;

}

if (key == curr->key) {

pred->next = curr->next;

delete curr;

LeaveCriticalSection(&glock);

return true;

} else {

LeaveCriticalSection(&glock);

return false;

}

}

Page 94: 242 naver-2

리스트의 구현

• 2 차 구현 : Lock의 세밀화 bool Remove(int key)

{

NODE *pred, *curr;

head.lock();

pred = &head; curr = pred->next;

curr->lock();

while (curr->key < key) {

pred->unlock();

pred = curr; curr = curr->next;

curr->lock();

}

if (key == curr->key) {

pred->next = curr->next;

curr->unlock(); pred->unlock();

delete curr; return true;

} else {

curr->unlock(); pred->unlock(); return false;

}

}

Page 95: 242 naver-2

리스트의 구현

• 3차 구현 : Lock감소

bool Remove(int key)

{

NODE *pred, *curr;

while(true) {

pred = &head;

curr = pred->next;

while (curr->key < key) {

pred = curr; curr = curr->next; }

pred->lock(); curr->lock();

if (!validate(pred, curr)) {

curr->unlock(); pred->unlock(); continue; }

if (key == curr->key) {

pred->next = curr->next;

curr->unlock(); pred->unlock();

// delete curr; return true;

} else {

curr->unlock(); pred->unlock(); return false; }

}

}

bool validate(NODE *pred, NODE *curr) {

NODE *node = &head;

while (node->key <= pred->key) {

if (node == pred) return pred->next == curr;

node = node->next;

}

return false;

}

Page 96: 242 naver-2

리스트의 구현

• 4차구현 : 마킹 활용

bool Remove(int key)

{

NODE *pred, *curr;

while(true) {

pred = &head;

curr = pred->next;

while (curr->key < key) {

pred = curr; curr = curr->next; }

pred->lock(); curr->lock();

if (!validate(pred, curr)) {

curr->unlock(); pred->unlock(); continue; }

if (key == curr->key) {

curr->marked = true;

pred->next = curr->next;

curr->unlock(); pred->unlock();

// delete curr; return true;

} else {

curr->unlock(); pred->unlock(); return false; }

}

}

bool validate(NODE *pred, NODE *curr) {

return (!pred->marked)

&& (!curr->marked)

&& (pred->next == curr);

}

Page 97: 242 naver-2

class LFNODE {

...

bool CompareAndSet(int old_v, int new_v)

{

int orig_v = InterlockedCompareExchange(reinterpret_cast<unsigned int *>(&next), new_v, old_v);

return orig_v == old_v;

}

bool CAS(LFNODE *old_node, LFNODE *new_node, bool oldMark, bool newMark) {

int oldvalue = reinterpret_cast<int>(old_node);

if (oldMark) oldvalue = oldvalue | 0x01;

else oldvalue = oldvalue & 0xFFFFFFFE;

int newvalue = reinterpret_cast<int>(new_node);

if (newMark) newvalue = newvalue | 0x01;

else newvalue = newvalue & 0xFFFFFFFE;

return CompareAndSet(oldvalue, newvalue);

}

bool AttemptMark(LFNODE *old_node, bool newMark) {

int oldvalue = reinterpret_cast<int>(old_node);

int newvalue = oldvalue;

if (newMark) newvalue = newvalue | 0x01;

else newvalue = newvalue & 0xFFFFFFFE;

return CompareAndSet(oldvalue, newvalue);

}

LFNODE *GetNextWithMark(bool *mark) {

int temp = reinterpret_cast<int>(next);

*mark = (0 != (temp & 0x01));

return reinterpret_cast<LFNODE *>(temp & 0xFFFFFFFE);

}

LFNODE *GetReference()

{

int temp = reinterpret_cast<int>(next);

return reinterpret_cast<LFNODE *>(temp & 0xFFFFFFFE);

}

};

LFNODE *AtomicMarkableReference(LFNODE *ptr, bool mark)

{

int temp = reinterpret_cast<int>(ptr);

if (mark) temp = temp | 0x01;

else temp = temp & 0xFFFFFFFE;

return reinterpret_cast<LFNODE *>(temp);

}

리스트의 구현

• 5차 구현 : Lock free

bool Remove(int key)

{

LFNODE *pred, *curr;

while(true) {

Find(&pred, &curr, key);

if (key != curr->key) return false;

LFNODE *succ = curr->GetReference();

if (false == curr->AttemptMark(succ, true)) continue;

pred->CAS(curr, succ, false, false);

return true;

}

}

void Find(LFNODE **Pred, LFNODE **Curr, int key)

{

LFNODE *pred = NULL;

LFNODE *curr = NULL;

LFNODE *succ = NULL;

bool marked = false;

ng_retry:

while(true) {

pred = &head;

curr = pred->GetReference();

while (true) {

succ = curr->GetNextWithMark(&marked);

while (marked) {

if (false == pred->CAS(curr, succ, false, false))

goto ng_retry;

curr = succ;

succ = curr->GetNextWithMark(&marked);

}

if (curr->key >= key) {

*Pred = pred; Curr = curr; return;

}

pred = curr; curr = succ;

}

}

}

Page 98: 242 naver-2

속도 비교

• 1과 1000사이의 숫자의 랜덤한 4백만회

삽입/삭제/검색 (i7-920)

2-98

쓰레드 개수 1차 2차 3차 4차 LockFree

1 0.715 3.350 1.800 0.914 0.864

2 0.992 2.723 1.267 0.668 0.589

4 0.972 1.575 0.691 0.355 0.350

8 0.970 1.199 0.463 0.278 0.247

16 0.999 1.180 0.552 0.250 0.273

Page 99: 242 naver-2

정리

• 공유메모리를 사용한 동기화는 사용하기 힘들다.

– 일관성, 중간 값

– Atomic memory의 한계

• 공유 자료 구조를 사용해야 한다.

• 좋은 공유 자료 구조는 만들기 힘들다.

– Non-blocking 알고리즘의 작성은 까다롭다.

– 상용 라이브러리도 좋다. Intel TBB, VS2010

PPL(Parallel Patterns Library)등

– ??NOBEL library, Concurrency Kit

2-99

Page 100: 242 naver-2

미래

• 그래도 멀티쓰레딩은 힘들다.

– 서버 프로그래머 연봉이 높은 이유

• Core가 늘어나면 지금 까지의 방법도 한계

– lock-free. wait-free overhead증가

– interlocked operation overhead증가

• 예측

– Transactional Memory

– 새로운 언어의 필요

• 예) Erlang, Haskell

2-100

Page 101: 242 naver-2

TIP

• 절대로 경험을 믿지 마라!!!

– 에러 날 확률이 로또 이하인 경우가 비일비재

– 디버깅 할 때, 사내 테스트 할 때는 멀쩡하다가

오픈베타 때 대형사고가 난다!!

– Correct가 증명된 알고리즘이나, 믿을 수 있는

회사에서 작성한 non-Blocking 프로그램을

사용하라.

• 자신이 만든 알고리즘이면 증명해봐라. (증명 방법은

교재 참조)

2-101

Page 102: 242 naver-2

TIP

• 클라우드환경은 다르다.

– 많은 가상머신에서 CompareAndSwap

오퍼레이션의 딜레이가 급증하는 현상이 있다.

– Parallels on MaxOS-X (OK)

– VMWare, Parallels, VirtualBox on Windows-7

(성능저하)

2-102

Page 103: 242 naver-2

NEXT

• 다음 발표(내년???)

– Lock-free 프로그래밍 근본적 이해

– 실제 MMO서버에서의 Lock-ree 성능 향상

– Transactional Memory with intel RTM

• 그 다음 발표???

− Lock-free search : SKIP-LIST

− ABA Problem, aka 효율적인 reference counting

− 고성능 MMO서버를 위한 non-blocking

자료구조의 활용

2-103

Page 104: 242 naver-2

Q&A

• 연락처

[email protected]

– 발표자료 : ftp://210.93.61.41 id:ndc21 passwd: 바람의나라

• 또는 www.slideshare.net 에서 발표제목 검색

• 참고자료

– Herlihy, Shavit, “The Art of Multiprocesor Programming, revised”,

Morgan Kaufman, 2012

– SEWELL, P., SARKAR, S., OWENS, S., NARDELLI, F. Z., AND

MYREEN, M. O. x86-tso: A rigorous and usable programmer’s

model for x86 multiprocessors. Communications of the ACM 53, 7

(July 2010), 89–97.

– INTEL, “Intel 64 and IA-32 Architectures Software Developer’s

Manual”, Vol 3A: System Programming Guide, Part 1

2-104