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


                발표자 소개
• KAIST 전산과 박사
  – 전공 : 멀티프로세서 CPU용 메모리 일관성 유지 HW
• NCSoft 근무
  – Alterlife , Project M 프로그램 팀장
• 현 : 한국산업기술대학교 게임공학과 부교수
  – 학부 강의 : 게임서버프로그래밍
  – 대학원 강의 : 멀티코어프로그래밍
2-3


                     목차
•   도입
•   현실
•   Welcome to the Hell.
•   새로운 희망
•   우리의 나아갈 길
•   실제성능
2-4


              도입
• 멀티쓰레드 프로그래밍 이란?
  – 멀티코어 혹은 멀티프로세서 컴퓨터의 성능을
    이끌어 내기 위한 프로그래밍 기법
  – 흑마술의 일종
   • 잘못 사용하면 패가 망신
2-5


              도입
• 흑마술 멀티쓰레드 프로그래밍의 위험성
  – “자꾸 죽는데 이유를 모르겠어요”
  • 자매품 : “이상한 값이 나오는데 이유를 모르겠어요”


 – “더 느려져요”
2-6


                     내용
•   도입
•   현실
•   Welcome to the Hell.
•   새로운 희망
•   우리의 나아갈 길
2-7


               현실
• “멀티쓰레드 안 해도 되지 않나요?”
   – NO!
   – “MultiThread 프로그래밍을 하지 않는 이상
     프로그램의 성능은 전혀 나아지지 않을 것임” –
     by Intel, AMD
   • “공짜 점심은 끝났어요~~”
2-8


               현실




피할 곳도 숨을 곳도 없습니다.
2-9


          현실
• 멀티쓰레드 프로그래밍을 하지 않으면?
  – (멀티코어) CPU가 놀아요.
  – 경쟁회사 제품보다 느려요.
  • FPS
  • 동접
2-10


                 현실
• 멀티 코어 CPU가 왜 나왔는가?
  – 예전에는 만들기 힘들어서? No
  – 다른 방법들의 약발이 다 떨어져서!
   • 클럭 속도, 캐시, 슈퍼스칼라, Out-of-order, 동적 분기
     예측…
 – 늦게 나온 이유
   • 프로그래머에게 욕을 먹을 것이 뻔하기 때문.
2-11


            현실
• 컴퓨터 공학을 전공했지만 학부에서
  가르치지 않았다.
• 큰맘 먹고 스터디를 시작했지만 한 달도 못
  가서 흐지부지 되었다. (원인은 다음 페이지)
• 그냥 멀티쓰레드 안 쓰기로 했다.
2-12


          현실
• 좋은 교재
2-13


              현실
• 왜 멀티쓰레드 프로그래밍이 어려운가?
  – 다른 쓰레드의 영향을 고려해서 프로그램 해야 하기
    때문에
  – 에러 재현과 디버깅이 힘들어서
  – Visual Studio가 사기를 치고 있기 때문
• 왜 멀티쓰레드 프로그래밍이 진짜로 어려운가?
  – CPU가 사기를 치고 있기 때문
2-14


                     내용
•   도입
•   현실
•   Welcome to the Hell.
•   새로운 희망
•   우리의 나아갈 길
2-15


                             고생길
• Visual Studio의 사기
 DWORD WINAPI ThreadFunc1(LPVOID lpVoid)
 {
          data = 1;
          flag = true;
 }



                            DWORD WINAPI ThreadFunc2(LPVOID lpVoid)
                            {
                                     while(!flag);
                                     my_data = data;
                            }
2-16


                             고생길
• Visual Studio의 사기                    DWORD WINAPI ThreadFunc2(LPVOID lpVOid)
                                       {
DWORD WINAPI ThreadFunc2(LPVOID   lpVoid)
                                       00951020 mov          al,byte ptr [flag (953374h)]
{
         while(!flag);                              while (!flag);
         my_data = data;                00951025   test        al,al
                                        00951027   je          ThreadFunc2+5 (951025h)
}                                                   int my_data = data;
                                                    printf("Data is %Xn", my_data);
                                        00951029   mov         eax,dword ptr [data (953370h)]
                                        0095102E   push        eax
                                        0095102F   push        offset string "Data is %Xn"
                                        (952104h)
                                        00951034 call          dword ptr [__imp__printf

싱글 쓰레드 프로그램이면?                          (9520ACh)]
                                        0095103A add           esp,8

    VS는 무죄!                                        return 0;
                                        0095103D xor           eax,eax
                                        }
                                        0095103F ret           4
2-17


                고생길
• Visual Studio의 사기를 피하는 방법
  – volatile을 사용하면 된다.
    • 반드시 메모리를 읽고 쓴다.
    • 변수를 레지스터에 할당하지 않는다.
    • 읽고 쓰는 순서를 지킨다.
  – 참 쉽죠?
  – “어셈블리를 모르면 Visual Studio의 사기를 알 수 없다”
   흠좀무…
2-18


                      고생길
• 정말 쉬운가???
 struct Qnode {
    volatile int data;
    volatile Qnode* next;
 };                                        무엇이 문제일까??
 DWORD WINAPI ThreadFunc1(LPVOID lpVoid)
 {
    while ( qnode->next == NULL ) { }
    my_data = qnode->next->data;
 }
고생길
• volatile의 사용법
   – volatile int * a;
      • *a = 1; // 순서를 지킴
      • a = b; // 순서를 지키지 않는다.
   – int * volatile a;
      • *a = 1; // 순서를 지키지 않음,
      • a = b; // 이것은 순서를 지킴
고생길
• Volatile 위치 오류의 예
     volatile Qnode*     next;               Qnode * volatile next;

   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;               011F1089 mov                      eax,dword       ptr [esi+4]
   }                                              011F108C lea                      esp,[esp]
                                                 011F1090       cmp                eax,ebx
                                                 011F1092       je                 ThreadFunc+90h (11F1090h)
01191090   mov             eax,dword ptr [esi+4]
01191093   test            eax,eax
01191095   je              ThreadFunc+90h (1191090h)
2-21


                                         고생길
• Thread 2개로 합계 1억을 만드는 프로그램
#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 %dn", sum);
                                                getchar();
                                                return 0;
                                     }
2-22


           고생길
• 다중 쓰레드 - 결과
2-23


          고생길
• 결과는?
• 엉뚱한 답
2-24


            고생길
• 왜 틀린 결과가 나왔을까?
  ─ “sum+=2”가 문제이다.
2-25


                    고생길
• 왜 틀린 결과가 나왔을까?
  – DATA RACE (복수의 쓰레드에서 같은 공유 메모리에
    동시에 WRITE하려는 행위.) 때문이다.
       쓰레드 1          쓰레드 2
     MOV EAX, SUM
                                   sum = 200
     ADD EAX, 2     MOV EAX, SUM
                                   sum = 200
     MOV SUM, EAX   ADD EAX, 2
                                   sum = 202
                    MOV SUM, EAX
                                   sum = 202
2-26


               고생길
• 해결 방법은?
  – Data Race가 있으면 멀티 쓰레드 프로그램이
    의도하지 않은 결과를 낼 수 있다.
  – Data Race를 없애면 된다.
• 어떻게
  – Lock과 Unlock을 사용한다.
2-27


       고생길
• 결과
2-28


                      고생길
• 결과가 옳게 나왔다. 만족하는가?
            실행시간      결과
 1 Thread   280577    100000000
 2 Thread   146823    50876664
 4 Thread   132362    27366758

            No LOCK                     실행시간      결과
                             1 Thread   2888071   100000000
                             2 Thread   5947291   100000000
                             4 Thread   4606754   100000000
     35배의 성능차이
                                        With LOCK
2-29


               고생길
• EnterCriticalSection() 이라는 물건은
  – 한번에 하나의 쓰레드만 실행 시킴
  – Lock을 얻지 못하면 시스템 호출
2-30


                    고생길
• 해결 방법은?
  – Lock을 쓰지 않으면 된다.
  – “Sum += 2”를 Atomic하게 만들면 된다.




• Atomic
  – 실행 중 다른 Core가 끼어들지 못하도록 한다.
2-31


                        고생길
• 결과가 옳게 나왔다. 만족하는가?
           실행시간       결과
1 Thread   280577     100000000
                                              실행시간      결과
2 Thread   146823     50876664
                                   1 Thread   1001528   100000000
4 Thread   132362     27366758
                                   2 Thread   1462121   100000000
            No LOCK                4 Thread   1452311   100000000
           실행시간      결과
1 Thread   2888071   100000000
                                        With InterlockedOperation
2 Thread   5947291   100000000
4 Thread   4606754   100000000

                                 With LOCK
2-32


         HELL
• 정답은?
2-33


                                고생길
• 만족하는가? (i7 – 4core)
            실행시간         결과                     실행시간        결과
 1 Thread   280,577      100000000   1 Thread   2,888,071   100000000
 2 Thread   146,823      50876664    2 Thread   5,947,291   100000000
 4 Thread   132,362      27366758    4 Thread   4,606,754   100000000

            No LOCK                             With LOCK
            실행시간        결과                      실행시간        결과

 1 Thread   1,001,528   100000000    1 Thread   28,7776     100000000

 2 Thread   1,462,121   100000000    2 Thread   15,6394     100000000

 4 Thread   1,452,311   100000000    4 Thread   96,925      100000000


 With InterlockedOperation                           정답
2-34


                              고생길
• 만족하는가? (XEON E5405, 2CPU)
              실행시간      결과                     실행시간        결과
 1 Thread     1,798     100000000   1 Thread   27,073      100000000
 2 Thread     2,926     520251348   2 Thread   83,586      100000000
 4 Thread     1,692     203771150   4 Thread   69,264      100000000
 8 Thread     3,699     193307522   8 Thread   67,156      100000000
              No LOCK                          With LOCK
               실행시간     결과                     실행시간        결과
   1 Thread    11,307   100000000   1 Thread   2055        100000000
   2 Thread    24,194   100000000   2 Thread   1798        100000000
   4 Thread    20,292   100000000   4 Thread   834         100000000
   8 Thread    18,699   100000000   8 Thread   419         100000000

 With InterlockedOperation                            정답
2-35


                  고생길
• 지금까지
  – Visual Studio의 마수에서 벗어나기
    • Volatile을 잘 쓰자
  – 경쟁상태 해결하기.
    • Lock을 최소화 하자
    • Lock대신 atomic operation을 사용하자
2-36


                  고생길
• 그러나
  – 절대로 모든 문제가 정답처럼 풀리지 않는다.
  – Interlocked로 구현 가능하면 다행 (atomic)
    • Interlock이 가능한 것은 일부 Instruction
  – 일반적인 자료구조를 Lock없이 Atomic하게
    구현하는 것은 큰 문제다.
  – Lock말고도 다른 문제가 있다.
2-37


            HELL
• 멀티 코어에서는 Data Race말고도 다른
 문제점이 있다.

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


            HELL


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


                    HELL
• 다음의 프로그램으로 Lock과 Unlock이 동작할까?
  – 피터슨 알고리즘               volatile int victim = 0;
                           volatile bool flag[2] = {false, false};
  – 두개의 쓰레드에서 Lock구현
                           Lock(int threadId)
  – 운영체제 교과서에 실려 있음        {
                                     int i = threadId;
  – threadId는 0과 1이라고 가정             j = 1 – i;
                                     flag[i] = true;
• 실행해 보자                             victim = i;
                                     while(flag[j] &&
                                               victim == i) {}
• 결과는?                     }
                           Unlock (int threadId)
                           {
                                     int i = threadId;
                                     flag[i] = false;
                           }
2-40


         HELL
• 이유는?
2-41


                    HELL
• 이유는?
  – CPU는 사기를 친다.
   • Line Based Cache Sharing
   • Out of order execution
   • write buffering
 – CPU는 프로그램을 순차적으로 실행하는
   척만한다.
   • 싱글코어에서는 절대로 들키지 않는다.
2-42


                  HELL
• Out-of-order 실행
  a = fsin(b);
  f = 3;

  a = b;   // a,b는 cache miss
  c = d;   // c,d는 cache hit
2-43


              HELL
• 문제는 메모리
  – 프로그램 순서대로 읽고 쓰지 않는다.
   • 읽기와 쓰기는 시간이 많이 걸리므로.
   • 옆의 프로세서(core)에서 보면 보인다.
• 어떠한 일이 벌어지는가?
2-44


                           HELL
• 아래의 두 개의 실행결과는 서로 다르다 어떠한 것이
 정확한 결과인가?
  thread a                thread b
  write (x, 1)            write(x, 2)
                                                 Type-A
   read(x, 2)             read(x, 2)

                                 thread a                 thread b

                 Type-B           write (x, 1)            write(x, 2)

                                  read(x, 1)              read(x, 1)
2-45


                     HELL
• 그러면 이것은?
    thread a           thread b
    write (x, 1)      write(x, 2)    Type-C!!
    read(x, 2)        read(x, 1)


                      thread a              thread b
         Type-D !!    write (x, 1)         read(y, 1)
                      write (y, 1)         read(x, 0)
2-46


                          HELL
• 그러면 이것은?
         thread a               thread b
         write (x, 1)          write(y, 1)         Type-E

         read (y, 0)           Read(x, 0)


             thread a      thread b          thread c       thread d
Type-F
           write (x, 1)   write (x, 2)       read(x, 1)     read(x, 2)
                                             read(x, 2)     read(x, 1)
2-47


                HELL
• 현실
  – 앞의 여러 형태의 결과는 전부 가능하다.
• 부정확해 보이는 결과가 나오는 이유?
  – 현재의 CPU는 Out-of-order실행을 한다.
  – Cache가 프로세서마다 따로 존재 하며 Cache는 64byte
    line별로 따로 관리된다.
  – 위의 2가지가 없다면 Computer는 몇백배 느려진다.
2-48


                                         HELL
• 메모리 쓰기 순서는 모든 코어에서 일치 하는가?
#define THREAD_MAX 2                 int main()
#define SIZE 10000000                {
volatile int x,y;
                                        DWORD addr;
int trace_x[SIZE], trace_y[SIZE];
                                        HANDLE hThread[THREAD_MAX];
DWORD WINAPI ThreadFunc0(LPVOID a)
{                                        .. // Thread 2개 실행
   for(int i = 0; i <SIZE;i++) {
      x = i;                             int count = 0;
      trace_y[i] = y;
                                         for (int i=0; i< SIZE;++i)
    }
   return 0;                                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;
DWORD WINAPI ThreadFunc1(LPVOID a)                  printf ("Error x : %d, trace[x]%d --", i, trace_x[i]);
{                                                   printf ("y : %d : trace[y] : %d n",
   for(int i = 0; i <SIZE;i++) {
                                                           trace_x[i], trace_y[trace_x[i]]);
      y = i;
      trace_x[i] = x;                          }
    }                                          printf("Total Memory Inconsistency:%dn", count);
   return 0;                             return 0;
}                                    }
2-49


                                  HELL
• 프로그램 설명


                     2     7          6    1
 8보다 3이 먼저 write!!   3     7          7    2
                     4     8          8    2       3보다 8이 먼저 write!!!
                     5     8          9    3
                     6     8         10    5
                     7     9         11    6
                     x   traceY       y   traceX
2-50


HELL




       공황상태…
2-51


                                 HELL
• 메모리에는 유령이…
 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;
 }
2-52


                              HELL
• 어떻게 실행했길래?
volatile int ARR[256];


bound = (int *) ((((((int) ARR) + 128) >> 7 ) << 7) - 2) ;
error = 0;
DWORD addr;

HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunc1, (LPVOID) 0, 0, &addr);
HANDLE hThread3 = CreateThread(NULL, 0, ThreadFunc2, (LPVOID) 1, 0, &addr);
2-53


             HELL
• 어떻게 실행했길래?
                          bound
                              2byte

     2byte




             Cache line
2-54


                          HELL
• 결과가….
 – 중간값
   • write시 최종값과 초기값이 아닌 다른 값이 도중에 메모리에 써지는
     현상                            short buf[256]

 – 이유는?                                    buf[0] = length;
                                           buf[1] = OP_MOVE;
    • Cache Line Size Boundary             *((float *)(&buf[2])) = x;
                                           *((float *)(&buf[4])) = y;
 – 대책은?                                    *((float *)(&buf[6])) = z;
                                           *((float *)(&buf[8])) = dx;
    • Pointer를 절대 믿지 마라.                   *((float *)(&buf[10])) = dy;
                                           *((float *)(&buf[12])) = dz;
    • Byte 밖에 믿을 수 없다.                     *((float *)(&buf[14])) = ax;
                                           *((float *)(&buf[16])) = ay;
    • Pointer가 아닌 변수는 Visual C++가 잘 해준다.   *((float *)(&buf[18])) = az;
                                           *((int *)(&buf[20])) = h;
                                           …
                                           send( fd, buf, (size_t)buf[0], 0 );
2-55


                      HELL
• 이러한 현상을 메모리 일관성(Memory Consistency)
 문제라고 부른다.




            http://en.wikipedia.org/wiki/Memory_ordering
2-56


                    HELL
• 어떻게 할 것인가?
 – 강제로 원하는 결과를 얻도록 한다.
    • 모든 메모리 접근을 Lock/Unlock으로 막으면 가능
      – 성능저하!!!
      – Lock은 어떻게 구현?
 – 위의 상황을 감안하고 프로그램 작성
    • 프로그래밍이 너무 어렵다.
      – 피터슨이나 빵집 알고리즘도 동작하지 않는다.


                어쩌라고???
2-57


                HELL
• 정리
  – 멀티쓰레드에서의 공유 메모리
   • 다른 코어에서 보았을 때 업데이트 순서가 틀릴 수 있다.
   • 메모리의 내용이 한 순간에 업데이트 되지 않을 때 도 있다.
 – 일반적인 프로그래밍 방식으로는 멀티쓰레드에서
  안정적으로 돌아가는 프로그램을 만들 수 없다.
2-58


                     내용
•   도입
•   현실
•   Welcome to the Hell.
•   새로운 희망
•   우리의 나아갈 길
•   실제성능
2-59


              희망
• 메모리에 대한 쓰기는 언젠가는 완료 된다.
• 자기 자신의 프로그램 순서는 지켜진다.
• 캐시의 일관성은 지켜진다.
  – 한번 지워졌던 값이 다시 살아나지는 않는다.
  – 언젠가는 모든 코어가 동일한 값을 본다
• 캐시라인 내부의 쓰기는 중간 값을 만들지 않는다.
2-60


                    희망
• 우리가 할 수 있는 것
  – CPU의 여러 삽질에도 불구 하고 Atomic Memory를 구현 할 수
    있다. 더군다나 Locking없이 SW적인 방법 만으로
  – 증명은 교재(아까 그 책) 참조
• Atomic
  – 접근(메모리는 read, write)의 절대 순서가 모든 쓰레드에서
    지켜지는 자료구조
  – 프로그래머가 필요로 하는 바로 그 자료구조
  – 싱글코어에서는 모든 메모리가 Atomic Memory이다.
2-61


                                 희망
•   Atomic Memory 만 있으면 되는가?
    – NO
    – 우리가 진짜로 필요로 하는 것은 멀티쓰레드에서 동작하는 효율적인
      자료구조들이다.
       • Queue, Stack, List, Map, Tree……
    – 즉 Atomic한 자료구조가 필요하다.
•   STL은???
    – 못 쓴다.
•   STL + LOCK은???
    – 느려서 못쓴다.
•   근데 “효율적인” 이라니?
2-62


          Lock없는 프로그램
• 효율적인 구현
  – Lock없는 구현
    • 성능 저하의 주범이므로 당연
  – Lock이 없다고 성능저하가 없는가??
    • 상대방 쓰레드에서 어떤 일을 해주기를 기다리는 한
      동시실행으로 인한 성능 개선을 얻기 힘들다.
      – while (other_thread.flag == true);
      – lock과 동일한 성능저하
    • 상대방 쓰레드의 행동에 의존적이지 않는 구현방식이 필요하다.
2-63


            Lock없는 프로그램
• 효율적인 구현
  – 블럭킹 (blocking)
     • 다른 쓰레드의 진행상태에 따라 진행이 막힐 수 있음
        – 예) while(lock != 0);
     • 멀티쓰레드의 bottle neck이 생긴다.
     • Lock을 사용하면 블럭킹
  – 넌블럭킹 (non-blocking)
     • 다른 쓰레드가 어떠한 삽질을 하고 있던 상관없이 진행
        – 예) 공유메모리 읽기/쓰기, Interlocked Operation
2-64


            Lock없는 프로그램
• 넌블럭킹의 등급
  – 무대기 (wait-free)
     • 모든 메소드가 정해진 유한한 단계에 실행을 끝마침
     • 멈춤 없는 프로그램 실행
  – 무잠금 (lock-free)
     •   무한히 많은 메소드가 유한한 단계에 실행을 끝마침
     •   무대기이면 무잠금이다
     •   기아(starvation)을 유발하기도 한다.
     •   성능을 위해 무대기 대신 무잠금을 선택하기도 한다.
2-65


          Lock없는 프로그램
• 넌블럭킹의 등급
  – 제한된 무대기 (bounded wait-free)
     • 유한하지만 쓰레드의 개수에 비례한 유한일 수 있다.
  – 무간섭 (obstruction-free)
     • 한 개를 제외한 모든 쓰레드가 멈추었을 때, 멈추지 않은
       쓰레드의 메소드가 유한한 단계에 종료할 때.
     • 충돌 중인 쓰레드를 잠깐 멈추게 할 필요가 있다.
2-66


           Lock없는 프로그램
• 정리
  – Wait-free, Lock-free
     • Lock을 사용하지 않고
     • 다른 쓰레드가 어떠한 행동을 하기를 기다리는 것
       없이
     • 자료구조의 접근을 Atomic하게 해주는 프로그램
2-67


        병행성과 정확성
• 그러면, Atomic Memory로 그런 자료구조를
 만들면 되지 않는가?

• Atomic Memory만으로는 다중 쓰레드
 무대기 큐를 만들 수 없다!!!!!!
 – (증명) : 아까 그 책
2-68


           병행성과 정확성
• 다중 쓰레드 무대기 큐를 만들려면?
  – 합의 (Consensus)객체가 필요하다.
    • 합의
      – 여러 개의 쓰레드 중 하나만 골라내어 그 선택자를 모두에게
        알려주는 API
      – 당연히 무대기
  – 실제 컴퓨터에 존재하는가?
    • 존재한다.
    • 특별한 CPU 명령으로 구현된다.
2-69


                합의(Consensus)
•   <넘어가자>
•   동기화 연산의 능력을 알아보는데 사용되는 알고리즘
•   합의 객체 의 정의 <PASS>
    – value_t decide(value_t value) 메소드를 구현
    – n 개의 스레드가 decide를 호출한다.
    – 하나의 스레드는 한번 이하로만 호출한다.
    – decide는 모든 호출에 대해 같은 값을 반환한다.
    – decde가 반환하는 값은 어떠한 스레드가 입력한 값이다.
2-70


                합의(Consensus)
• 구현 (Windows API)
  – InterlockedCompareExchange()
   LONG __cdecl InterlockedCompareExchange(
            __inout LONG volatile *Destination,
            __in LONG Exchange,
            __in LONG Comparand );

  – Destination에 저장된 값과 Comparand가 같은 값이면
    Destination에 Exchange를 쓴다.
  – Destination에 저장되어 있던 값을 리턴한다.
  – Atomic하다.
2-71


          합의(Consensus)
• 실제 x86 CPU상의 구현
  – lock prefix와 cmpxchg 명령어로 구현
  – lock cmpxchg [A], b 기계어 명령으로 구현
    • eax에 비교값, A에 주소, b에 넣을 값
• EnterCriticalSection()도 위와 같은 어셈블리를
 사용해서 구현되어 있다.
  – 하지만 lock을 얻지 못하면 Windows kernel 호출크리.
2-72


      합의(Consensus)
• 합의의 위용
  – 모든 자료구조를 멀티쓰레드 무대기자료구조로
    만들 수 있다.
  – 바꿔주는 프로그램이 있다.
  – STL도 OK!
2-73


                   합의(Consensus)
Response Apply(Invocation Invoc, int threadId) {
Node *prefer = new Node(Invoc);
while (prefer->seq == 0) {
   Node *before = tail.GetMax(head);
   LONG decide_prefer = reinterpret_cast<LONG>(prefer);
   Node *after = reinterpret_cast<Node*>( before->decideNext.Decide(decide_prefer) );
   before->next = after;
   after->seq = before->seq + 1;
   head[threadId] = after;
}
SeqQueue myObject;

Node *current = tail.next;
while (current != prefer) {
   myObject.Apply(current->invoc);
   current = current->next;
}
return myObject.Apply(current->invoc);
2-74


                  희망
• Happy End????
  – NO
• 합의를 가지고 자료구조를 <효율적인> 무대기나
 무잠금으로 구현하는 것은 매우 어려운 일이다.
  – 구현은 어렵지 않으나 성능은 안습이다.
성능 비교
•   네할렘 XEON, E5520, 2.3GHz, 2CPU (8 core)
•   STL의 queue를 무잠금, 무대기로 구현한 것과, CriticalSection으로
    atomic하게 만든것의 성능 비교.
    – Test조건 : 16374번 Enqueue, Dequeue (결과는 mili second)
    – EnterCriticaSection()을 사용한 것은 테스트 데이터의 크기가 128배
    – 따라서 50000배 성능 차이 (4개 thread의 경우)
               쓰레드 갯수           1      2      4      8      16
               무잠금 만능          5091   7445   6811   10004   6846
               무대기 만능          5021   7257   7026   9170    6466
               EnterCritical   222    238    219    254     257


• EnterCriticalSection을 사용해야 하는가?
    –   No : 멀티쓰레드에서의 성능향상이 없다.
2-76


               희망
• 결론
  – CPU가 제공하는 Consensus를 사용하면 모든
    싱글쓰레드 알고리즘을 Lock-free한 멀티쓰레드
    알고리즘으로 변환할 수 있다.
• 현실
  – 비효율 적이다.
2-77


                            희망
• 대안
  – 자료구조에 맞추어 최적화된 lock-free알고리즘을 일일이 개발해야
    한다.
     • 멀티쓰레드 프로그램은 힘들다. => 연봉이 높다.
• 다른 데서 구해 쓸 수도 있다.
  – Intel TBB, VS2010 PPL
  – 인터넷
  – 하지만 범용적일 수록 성능이 떨어진다. 자신에게 딱 맞는 것을
    만드는 것이 좋다.
2-78


                     내용
•   도입
•   현실
•   Welcome to the Hell.
•   새로운 희망
•   우리의 나아갈 길
•   실제성능
2-79


                    실제 성능
• 속도 비교 예제)
  – Set의 구현
   • 중복 불가
   • 정렬되어 있음
 – Add, Remove, Contains 메소드
 – 여러 가지 병렬화 기법 사용
   • Course, Fine, Optimistic, Fine, Lock-Free
 – 인용) 아까 그 책
2-80


             실제 성능
• Course Set          EnterCriticalSection(&lock);
                      pred = &head;
                      curr = pred->next;
  – 모든 메소드에 Locking
                      while (curr->key < key) {
                                pred = curr;
                                curr = curr->next;
                      }
                      if (key == curr->key) {
                                pred->next = curr->next;
                                delete curr;
                                LeaveCriticalSection(&lock);
                                return true;
                      } else {
                                LeaveCriticalSection(&lock);
                                return false;
                      }
2-81


              실제 성능
• Fine Set          head.Lock();
                    pred = &head;
                    curr = pred->next;
  – 수정이 필요한         curr->Lock();
                    while (curr->key < key) {
                              pred->Unlock();
    Node만 Locking             pred = curr;
                              curr = curr->next;
                              curr->Lock();
                    }
                    if (curr->key == key) {
                              pred->next = curr->next;
                              curr->Unlock();
                              pred->Unlock();
                              return true;
                    }
                    curr->Unlock();
                    pred->Unlock();
                    return false;
2-82


                   실제 성능
                    while (true) {
• Optimistic Set       Node *pred = &head;
                       Node *curr = pred->next;
                       while (curr->key < key) {
  – 수정이 필요한               pred = curr;
                          curr = curr->next;
    Node검색을            }
                       pred->Lock();    curr->Lock();
    Locking없이 하고       if (validate(pred, curr)) {
                          if (curr->key == key) {
  – Locking 후 확인             pred->next = curr->next;
                             pred->Unlock();    curr->Unlock();
                             // delete curr;
                             return true;
                          } else {
                             pred->Unlock();    curr->Unlock();
                             return false;
                          }
                       }
                       pred->Unlock();    curr->Unlock();
                    }
2-83


                실제 성능
• Lazy Set
                      while (true) {
                         Node *pred = &head;
                         Node *curr = head.next;
                         while (curr->key < key) {
  – 지운 노드를 직접               pred = curr;
                            curr = curr->next;
    지우지 않고               }
                         pred->Lock();    curr->Lock();

    marking만 한다.         if (validate(pred, curr)) {
                            if (curr->key != key) {
                                  curr->Unlock();   pred->Unlock();
  – Contain()에서                   return false;
                            } else {

    잠금을 없앰                        curr->marked = true;
                                  pred->next = curr->next;
                                  curr->Unlock(); pred->Unlock();
  – Optimistic의 최적화         }
                                  return true;

                            curr->Unlock(); pred->Unlock();
                         }
                         curr->Unlock();   pred->Unlock();
                      }
2-84


                    실제 성능
• Lock-free Set             bool snip;
                            while (true) {
  – Lock()사용안함                 Window *window = find(&head, key);
                               Node *pred = window->pred;
  – Next필드와 marked필드를          Node *curr = window->curr;
    동시에 atomic하게               if (curr->key != key) return false;
                               else {
    업데이트 하는 것을 구현                 Node *succ = GetReference(curr->next);
  – Pointer의 LSB를 marked로         snip = curr->attemptMark(succ, true);
                                  if (!snip) continue;
    사용                            pred->CompareAndSet(curr, succ, false,
                            false);
                                  return true;
                               }
                            }
2-85


                              실제 성능
• Lock-free Set
   – CompareAndSet의 구현
 bool CompareAndSet(Node *old_node, Node *next_node, bool old_mark, bool next_mark) {
    DWORD old_addr = reinterpret_cast<DWORD>(old_node);
    if (old_mark) old_addr = old_addr | 0x1;
    else old_addr = old_addr & 0xfffffffe;
    DWORD next_addr = reinterpret_cast<DWORD>(next_node);
    if (next_mark) next_addr = next_addr | 0x1;
    else next_addr = next_addr & 0xfffffffe;
    int prev_addr = InterlockedCompareExchange(reinterpret_cast<long *>(&next),
                                         next_addr, old_addr);
    return (prev_addr == old_addr);
 }
2-86


                   속도 비교
• Intel i7 920 (단위: 초)
   – 32767번 랜덤한 숫자 추가, 삭제 반복
 쓰레드 개수   Course   Fine    Optimistic Lazy   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
2-87


                실제 성능
• 우리의 목적
  – 고성능
  – 쉬운 프로그래밍
• 번역하면
  – 무대기 (wait-free)
  – 멀티쓰레드 자료구조 (Queue, Stack, List~~~)
2-88


                     정리
• 지향하는 프로그래밍 스타일
  – Lock을 사용한 프로그래밍
   • Blocking
   • 느림 (몇 백배)
 – Atomic Memory를 사용한 프로그래밍
   • 표현력이 떨어짐
   • Queue도 만들지 못함
 – 무대기 자료구조를 사용한 프로그래밍
   • OK
2-89


             정리
• 무대기(wait-free) 자료구조를 사용한
 프로그래밍
 – 각각의 자료구조를 Lock을 사용하지 않고
   구현해야 한다.
 – Lock은 사용하지 않지만 합의객체는 필요하다.
   (합의 객체 없이는 만들 수 없음이 증명되어
   있다.)
2-90


                            정리
•   멀티 스레드 프로그래밍은 공유 메모리를 사용해서 스레드간의 협업을
    한다.
•   공유메모리를 사용한 동기화는 사용하기 힘들다.
    – 일관성, 중간 값
    – Atomic memory의 한계
•   공유 자료 구조를 만들어서 사용하는 것이 좋다.
•   좋은 공유 자료 구조는 만들기 힘들다.
    – 무대기(wait free)알고리즘의 작성은 까다롭다.
    – 상용 라이브러리도 좋다. Intel TBB, VS2010 PPL(Parallel Patterns
      Library)등
2-91


                            미래
• 그래도 멀티쓰레딩은 힘들다.
 – 멀티쓰레드 프로그래머 연봉이 높은 이유
• Core가 늘어나면 지금 까지의 방법도 한계
 – lock-free. wait-free overhead증가
 – interlocked operation overhead증가
• 예측
 – Transactional Memory
 – 새로운 언어의 필요
    • 예) Earlang, Haskell
2-92


                                                 TIP
•   Nehalem으로 업그레이드 하라.
     –   좋다! QPI만세
     –   Opteron을 쓸지언정 절대 구형 XEON은 쓰지 말아라.
•   Lock을 쓸 때 Spinlock은 TTS을 사용하고 일정 회전 이후는 Sleep()으로 쓰레드를 재워라.
     –   RWLock(Reader Write Lock)을 사용할 수 있으면 하라.
•   수정 후에는 반드시 Testing
     –   Lock이 허름한 무대기/무잠금보다 성능이 높은 경우가 많다.
•   메모리관리는 따로 해라
     –   tsmalloc(thread cashing malloc)도 괜찮다.
•   인텔 매뉴얼을 믿지 마라.
     –   CPU 실행방식 변경가능, CPU가 Arm으로 바뀔 수도 있다.
     –   매뉴얼 믿고 쓰다가 뒤통수 (너무 고약하다)
     –   자주 바뀌는 매뉴얼
•   Worst Case 튜닝
     –   저부하 일때 CPU낭비 상관 없음
•   Wait-free는 Lock-free에서 starvation을 제거한 것
2-93


                   NEXT
• 다음 강의(내년???)
  – Lock-free search : SKIP-LIST
  – 효율적인 LOCK : tts, back-off, spin-count 고찰
  – Next Solution
     • Stateless 프로그래밍 언어
     • Transactional Memory
  – ABA Problem, aka 효율적인 reference counting
2-94


                                    Q&A
•   연락처
    – nhjung@kpu.ac.kr
    – <제작중> http://wsl.kpu.ac.kr/~gameserver/index.html
    – 발표자료 : ftp://210.93.61.41 id:ndc21 passwd: <바람의나라>
•   참고자료
    – Herlihy, Shavit, “The Art of Multiprocesor Programming”, Morgan Kaufman, 2008
    – 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-95


                    광고
• 체계적으로 배우고 싶으신 분
  – 한국산업기술대학교 게임공학과 대학원
  – 회사를 설득해서 계약 체결
   •   JCE와 계약 경험(win-win)
   •   회사로 출장 강의
   •   등록금 공짜 (정부지원)
   •   석사학위 취득

Ndc12 2

  • 1.
    멀티쓰레드 프로그래밍이 왜이리 힘드나요? (컴파일러와 하드웨어에서 Lock-free 알고리즘 까지) 정내훈 한국산업기술대학교 게임공학과
  • 2.
    2-2 발표자 소개 • KAIST 전산과 박사 – 전공 : 멀티프로세서 CPU용 메모리 일관성 유지 HW • NCSoft 근무 – Alterlife , Project M 프로그램 팀장 • 현 : 한국산업기술대학교 게임공학과 부교수 – 학부 강의 : 게임서버프로그래밍 – 대학원 강의 : 멀티코어프로그래밍
  • 3.
    2-3 목차 • 도입 • 현실 • Welcome to the Hell. • 새로운 희망 • 우리의 나아갈 길 • 실제성능
  • 4.
    2-4 도입 • 멀티쓰레드 프로그래밍 이란? – 멀티코어 혹은 멀티프로세서 컴퓨터의 성능을 이끌어 내기 위한 프로그래밍 기법 – 흑마술의 일종 • 잘못 사용하면 패가 망신
  • 5.
    2-5 도입 • 흑마술 멀티쓰레드 프로그래밍의 위험성 – “자꾸 죽는데 이유를 모르겠어요” • 자매품 : “이상한 값이 나오는데 이유를 모르겠어요” – “더 느려져요”
  • 6.
    2-6 내용 • 도입 • 현실 • Welcome to the Hell. • 새로운 희망 • 우리의 나아갈 길
  • 7.
    2-7 현실 • “멀티쓰레드 안 해도 되지 않나요?” – NO! – “MultiThread 프로그래밍을 하지 않는 이상 프로그램의 성능은 전혀 나아지지 않을 것임” – by Intel, AMD • “공짜 점심은 끝났어요~~”
  • 8.
    2-8 현실 피할 곳도 숨을 곳도 없습니다.
  • 9.
    2-9 현실 • 멀티쓰레드 프로그래밍을 하지 않으면? – (멀티코어) CPU가 놀아요. – 경쟁회사 제품보다 느려요. • FPS • 동접
  • 10.
    2-10 현실 • 멀티 코어 CPU가 왜 나왔는가? – 예전에는 만들기 힘들어서? No – 다른 방법들의 약발이 다 떨어져서! • 클럭 속도, 캐시, 슈퍼스칼라, Out-of-order, 동적 분기 예측… – 늦게 나온 이유 • 프로그래머에게 욕을 먹을 것이 뻔하기 때문.
  • 11.
    2-11 현실 • 컴퓨터 공학을 전공했지만 학부에서 가르치지 않았다. • 큰맘 먹고 스터디를 시작했지만 한 달도 못 가서 흐지부지 되었다. (원인은 다음 페이지) • 그냥 멀티쓰레드 안 쓰기로 했다.
  • 12.
    2-12 현실 • 좋은 교재
  • 13.
    2-13 현실 • 왜 멀티쓰레드 프로그래밍이 어려운가? – 다른 쓰레드의 영향을 고려해서 프로그램 해야 하기 때문에 – 에러 재현과 디버깅이 힘들어서 – Visual Studio가 사기를 치고 있기 때문 • 왜 멀티쓰레드 프로그래밍이 진짜로 어려운가? – CPU가 사기를 치고 있기 때문
  • 14.
    2-14 내용 • 도입 • 현실 • Welcome to the Hell. • 새로운 희망 • 우리의 나아갈 길
  • 15.
    2-15 고생길 • Visual Studio의 사기 DWORD WINAPI ThreadFunc1(LPVOID lpVoid) { data = 1; flag = true; } DWORD WINAPI ThreadFunc2(LPVOID lpVoid) { while(!flag); my_data = data; }
  • 16.
    2-16 고생길 • Visual Studio의 사기 DWORD WINAPI ThreadFunc2(LPVOID lpVOid) { DWORD WINAPI ThreadFunc2(LPVOID lpVoid) 00951020 mov al,byte ptr [flag (953374h)] { while(!flag); while (!flag); my_data = data; 00951025 test al,al 00951027 je ThreadFunc2+5 (951025h) } int my_data = data; printf("Data is %Xn", my_data); 00951029 mov eax,dword ptr [data (953370h)] 0095102E push eax 0095102F push offset string "Data is %Xn" (952104h) 00951034 call dword ptr [__imp__printf 싱글 쓰레드 프로그램이면? (9520ACh)] 0095103A add esp,8 VS는 무죄! return 0; 0095103D xor eax,eax } 0095103F ret 4
  • 17.
    2-17 고생길 • Visual Studio의 사기를 피하는 방법 – volatile을 사용하면 된다. • 반드시 메모리를 읽고 쓴다. • 변수를 레지스터에 할당하지 않는다. • 읽고 쓰는 순서를 지킨다. – 참 쉽죠? – “어셈블리를 모르면 Visual Studio의 사기를 알 수 없다” 흠좀무…
  • 18.
    2-18 고생길 • 정말 쉬운가??? struct Qnode { volatile int data; volatile Qnode* next; }; 무엇이 문제일까?? DWORD WINAPI ThreadFunc1(LPVOID lpVoid) { while ( qnode->next == NULL ) { } my_data = qnode->next->data; }
  • 19.
    고생길 • volatile의 사용법 – volatile int * a; • *a = 1; // 순서를 지킴 • a = b; // 순서를 지키지 않는다. – int * volatile a; • *a = 1; // 순서를 지키지 않음, • a = b; // 이것은 순서를 지킴
  • 20.
    고생길 • Volatile 위치오류의 예 volatile Qnode* next; Qnode * volatile next; 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; 011F1089 mov eax,dword ptr [esi+4] } 011F108C lea esp,[esp] 011F1090 cmp eax,ebx 011F1092 je ThreadFunc+90h (11F1090h) 01191090 mov eax,dword ptr [esi+4] 01191093 test eax,eax 01191095 je ThreadFunc+90h (1191090h)
  • 21.
    2-21 고생길 • Thread 2개로 합계 1억을 만드는 프로그램 #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 %dn", sum); getchar(); return 0; }
  • 22.
    2-22 고생길 • 다중 쓰레드 - 결과
  • 23.
    2-23 고생길 • 결과는? • 엉뚱한 답
  • 24.
    2-24 고생길 • 왜 틀린 결과가 나왔을까? ─ “sum+=2”가 문제이다.
  • 25.
    2-25 고생길 • 왜 틀린 결과가 나왔을까? – DATA RACE (복수의 쓰레드에서 같은 공유 메모리에 동시에 WRITE하려는 행위.) 때문이다. 쓰레드 1 쓰레드 2 MOV EAX, SUM sum = 200 ADD EAX, 2 MOV EAX, SUM sum = 200 MOV SUM, EAX ADD EAX, 2 sum = 202 MOV SUM, EAX sum = 202
  • 26.
    2-26 고생길 • 해결 방법은? – Data Race가 있으면 멀티 쓰레드 프로그램이 의도하지 않은 결과를 낼 수 있다. – Data Race를 없애면 된다. • 어떻게 – Lock과 Unlock을 사용한다.
  • 27.
    2-27 고생길 • 결과
  • 28.
    2-28 고생길 • 결과가 옳게 나왔다. 만족하는가? 실행시간 결과 1 Thread 280577 100000000 2 Thread 146823 50876664 4 Thread 132362 27366758 No LOCK 실행시간 결과 1 Thread 2888071 100000000 2 Thread 5947291 100000000 4 Thread 4606754 100000000 35배의 성능차이 With LOCK
  • 29.
    2-29 고생길 • EnterCriticalSection() 이라는 물건은 – 한번에 하나의 쓰레드만 실행 시킴 – Lock을 얻지 못하면 시스템 호출
  • 30.
    2-30 고생길 • 해결 방법은? – Lock을 쓰지 않으면 된다. – “Sum += 2”를 Atomic하게 만들면 된다. • Atomic – 실행 중 다른 Core가 끼어들지 못하도록 한다.
  • 31.
    2-31 고생길 • 결과가 옳게 나왔다. 만족하는가? 실행시간 결과 1 Thread 280577 100000000 실행시간 결과 2 Thread 146823 50876664 1 Thread 1001528 100000000 4 Thread 132362 27366758 2 Thread 1462121 100000000 No LOCK 4 Thread 1452311 100000000 실행시간 결과 1 Thread 2888071 100000000 With InterlockedOperation 2 Thread 5947291 100000000 4 Thread 4606754 100000000 With LOCK
  • 32.
    2-32 HELL • 정답은?
  • 33.
    2-33 고생길 • 만족하는가? (i7 – 4core) 실행시간 결과 실행시간 결과 1 Thread 280,577 100000000 1 Thread 2,888,071 100000000 2 Thread 146,823 50876664 2 Thread 5,947,291 100000000 4 Thread 132,362 27366758 4 Thread 4,606,754 100000000 No LOCK With LOCK 실행시간 결과 실행시간 결과 1 Thread 1,001,528 100000000 1 Thread 28,7776 100000000 2 Thread 1,462,121 100000000 2 Thread 15,6394 100000000 4 Thread 1,452,311 100000000 4 Thread 96,925 100000000 With InterlockedOperation 정답
  • 34.
    2-34 고생길 • 만족하는가? (XEON E5405, 2CPU) 실행시간 결과 실행시간 결과 1 Thread 1,798 100000000 1 Thread 27,073 100000000 2 Thread 2,926 520251348 2 Thread 83,586 100000000 4 Thread 1,692 203771150 4 Thread 69,264 100000000 8 Thread 3,699 193307522 8 Thread 67,156 100000000 No LOCK With LOCK 실행시간 결과 실행시간 결과 1 Thread 11,307 100000000 1 Thread 2055 100000000 2 Thread 24,194 100000000 2 Thread 1798 100000000 4 Thread 20,292 100000000 4 Thread 834 100000000 8 Thread 18,699 100000000 8 Thread 419 100000000 With InterlockedOperation 정답
  • 35.
    2-35 고생길 • 지금까지 – Visual Studio의 마수에서 벗어나기 • Volatile을 잘 쓰자 – 경쟁상태 해결하기. • Lock을 최소화 하자 • Lock대신 atomic operation을 사용하자
  • 36.
    2-36 고생길 • 그러나 – 절대로 모든 문제가 정답처럼 풀리지 않는다. – Interlocked로 구현 가능하면 다행 (atomic) • Interlock이 가능한 것은 일부 Instruction – 일반적인 자료구조를 Lock없이 Atomic하게 구현하는 것은 큰 문제다. – Lock말고도 다른 문제가 있다.
  • 37.
    2-37 HELL • 멀티 코어에서는 Data Race말고도 다른 문제점이 있다. • “상상한 것 그 이상을 보여준다”, 충공깽
  • 38.
    2-38 HELL EnterCriticalSection()이 문제가 있으니 나만의 Lock을 구현해 볼까?
  • 39.
    2-39 HELL • 다음의 프로그램으로 Lock과 Unlock이 동작할까? – 피터슨 알고리즘 volatile int victim = 0; volatile bool flag[2] = {false, false}; – 두개의 쓰레드에서 Lock구현 Lock(int threadId) – 운영체제 교과서에 실려 있음 { int i = threadId; – threadId는 0과 1이라고 가정 j = 1 – i; flag[i] = true; • 실행해 보자 victim = i; while(flag[j] && victim == i) {} • 결과는? } Unlock (int threadId) { int i = threadId; flag[i] = false; }
  • 40.
    2-40 HELL • 이유는?
  • 41.
    2-41 HELL • 이유는? – CPU는 사기를 친다. • Line Based Cache Sharing • Out of order execution • write buffering – CPU는 프로그램을 순차적으로 실행하는 척만한다. • 싱글코어에서는 절대로 들키지 않는다.
  • 42.
    2-42 HELL • Out-of-order 실행 a = fsin(b); f = 3; a = b; // a,b는 cache miss c = d; // c,d는 cache hit
  • 43.
    2-43 HELL • 문제는 메모리 – 프로그램 순서대로 읽고 쓰지 않는다. • 읽기와 쓰기는 시간이 많이 걸리므로. • 옆의 프로세서(core)에서 보면 보인다. • 어떠한 일이 벌어지는가?
  • 44.
    2-44 HELL • 아래의 두 개의 실행결과는 서로 다르다 어떠한 것이 정확한 결과인가? thread a thread b write (x, 1) write(x, 2) Type-A read(x, 2) read(x, 2) thread a thread b Type-B write (x, 1) write(x, 2) read(x, 1) read(x, 1)
  • 45.
    2-45 HELL • 그러면 이것은? thread a thread b write (x, 1) write(x, 2) Type-C!! read(x, 2) read(x, 1) thread a thread b Type-D !! write (x, 1) read(y, 1) write (y, 1) read(x, 0)
  • 46.
    2-46 HELL • 그러면 이것은? thread a thread b write (x, 1) write(y, 1) Type-E read (y, 0) Read(x, 0) thread a thread b thread c thread d Type-F write (x, 1) write (x, 2) read(x, 1) read(x, 2) read(x, 2) read(x, 1)
  • 47.
    2-47 HELL • 현실 – 앞의 여러 형태의 결과는 전부 가능하다. • 부정확해 보이는 결과가 나오는 이유? – 현재의 CPU는 Out-of-order실행을 한다. – Cache가 프로세서마다 따로 존재 하며 Cache는 64byte line별로 따로 관리된다. – 위의 2가지가 없다면 Computer는 몇백배 느려진다.
  • 48.
    2-48 HELL • 메모리 쓰기 순서는 모든 코어에서 일치 하는가? #define THREAD_MAX 2 int main() #define SIZE 10000000 { volatile int x,y; DWORD addr; int trace_x[SIZE], trace_y[SIZE]; HANDLE hThread[THREAD_MAX]; DWORD WINAPI ThreadFunc0(LPVOID a) { .. // Thread 2개 실행 for(int i = 0; i <SIZE;i++) { x = i; int count = 0; trace_y[i] = y; for (int i=0; i< SIZE;++i) } return 0; 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; DWORD WINAPI ThreadFunc1(LPVOID a) printf ("Error x : %d, trace[x]%d --", i, trace_x[i]); { printf ("y : %d : trace[y] : %d n", for(int i = 0; i <SIZE;i++) { trace_x[i], trace_y[trace_x[i]]); y = i; trace_x[i] = x; } } printf("Total Memory Inconsistency:%dn", count); return 0; return 0; } }
  • 49.
    2-49 HELL • 프로그램 설명 2 7 6 1 8보다 3이 먼저 write!! 3 7 7 2 4 8 8 2 3보다 8이 먼저 write!!! 5 8 9 3 6 8 10 5 7 9 11 6 x traceY y traceX
  • 50.
    2-50 HELL 공황상태…
  • 51.
    2-51 HELL • 메모리에는 유령이… 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; }
  • 52.
    2-52 HELL • 어떻게 실행했길래? volatile int ARR[256]; bound = (int *) ((((((int) ARR) + 128) >> 7 ) << 7) - 2) ; error = 0; DWORD addr; HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunc1, (LPVOID) 0, 0, &addr); HANDLE hThread3 = CreateThread(NULL, 0, ThreadFunc2, (LPVOID) 1, 0, &addr);
  • 53.
    2-53 HELL • 어떻게 실행했길래? bound 2byte 2byte Cache line
  • 54.
    2-54 HELL • 결과가…. – 중간값 • write시 최종값과 초기값이 아닌 다른 값이 도중에 메모리에 써지는 현상 short buf[256] – 이유는? buf[0] = length; buf[1] = OP_MOVE; • Cache Line Size Boundary *((float *)(&buf[2])) = x; *((float *)(&buf[4])) = y; – 대책은? *((float *)(&buf[6])) = z; *((float *)(&buf[8])) = dx; • Pointer를 절대 믿지 마라. *((float *)(&buf[10])) = dy; *((float *)(&buf[12])) = dz; • Byte 밖에 믿을 수 없다. *((float *)(&buf[14])) = ax; *((float *)(&buf[16])) = ay; • Pointer가 아닌 변수는 Visual C++가 잘 해준다. *((float *)(&buf[18])) = az; *((int *)(&buf[20])) = h; … send( fd, buf, (size_t)buf[0], 0 );
  • 55.
    2-55 HELL • 이러한 현상을 메모리 일관성(Memory Consistency) 문제라고 부른다. http://en.wikipedia.org/wiki/Memory_ordering
  • 56.
    2-56 HELL • 어떻게 할 것인가? – 강제로 원하는 결과를 얻도록 한다. • 모든 메모리 접근을 Lock/Unlock으로 막으면 가능 – 성능저하!!! – Lock은 어떻게 구현? – 위의 상황을 감안하고 프로그램 작성 • 프로그래밍이 너무 어렵다. – 피터슨이나 빵집 알고리즘도 동작하지 않는다. 어쩌라고???
  • 57.
    2-57 HELL • 정리 – 멀티쓰레드에서의 공유 메모리 • 다른 코어에서 보았을 때 업데이트 순서가 틀릴 수 있다. • 메모리의 내용이 한 순간에 업데이트 되지 않을 때 도 있다. – 일반적인 프로그래밍 방식으로는 멀티쓰레드에서 안정적으로 돌아가는 프로그램을 만들 수 없다.
  • 58.
    2-58 내용 • 도입 • 현실 • Welcome to the Hell. • 새로운 희망 • 우리의 나아갈 길 • 실제성능
  • 59.
    2-59 희망 • 메모리에 대한 쓰기는 언젠가는 완료 된다. • 자기 자신의 프로그램 순서는 지켜진다. • 캐시의 일관성은 지켜진다. – 한번 지워졌던 값이 다시 살아나지는 않는다. – 언젠가는 모든 코어가 동일한 값을 본다 • 캐시라인 내부의 쓰기는 중간 값을 만들지 않는다.
  • 60.
    2-60 희망 • 우리가 할 수 있는 것 – CPU의 여러 삽질에도 불구 하고 Atomic Memory를 구현 할 수 있다. 더군다나 Locking없이 SW적인 방법 만으로 – 증명은 교재(아까 그 책) 참조 • Atomic – 접근(메모리는 read, write)의 절대 순서가 모든 쓰레드에서 지켜지는 자료구조 – 프로그래머가 필요로 하는 바로 그 자료구조 – 싱글코어에서는 모든 메모리가 Atomic Memory이다.
  • 61.
    2-61 희망 • Atomic Memory 만 있으면 되는가? – NO – 우리가 진짜로 필요로 하는 것은 멀티쓰레드에서 동작하는 효율적인 자료구조들이다. • Queue, Stack, List, Map, Tree…… – 즉 Atomic한 자료구조가 필요하다. • STL은??? – 못 쓴다. • STL + LOCK은??? – 느려서 못쓴다. • 근데 “효율적인” 이라니?
  • 62.
    2-62 Lock없는 프로그램 • 효율적인 구현 – Lock없는 구현 • 성능 저하의 주범이므로 당연 – Lock이 없다고 성능저하가 없는가?? • 상대방 쓰레드에서 어떤 일을 해주기를 기다리는 한 동시실행으로 인한 성능 개선을 얻기 힘들다. – while (other_thread.flag == true); – lock과 동일한 성능저하 • 상대방 쓰레드의 행동에 의존적이지 않는 구현방식이 필요하다.
  • 63.
    2-63 Lock없는 프로그램 • 효율적인 구현 – 블럭킹 (blocking) • 다른 쓰레드의 진행상태에 따라 진행이 막힐 수 있음 – 예) while(lock != 0); • 멀티쓰레드의 bottle neck이 생긴다. • Lock을 사용하면 블럭킹 – 넌블럭킹 (non-blocking) • 다른 쓰레드가 어떠한 삽질을 하고 있던 상관없이 진행 – 예) 공유메모리 읽기/쓰기, Interlocked Operation
  • 64.
    2-64 Lock없는 프로그램 • 넌블럭킹의 등급 – 무대기 (wait-free) • 모든 메소드가 정해진 유한한 단계에 실행을 끝마침 • 멈춤 없는 프로그램 실행 – 무잠금 (lock-free) • 무한히 많은 메소드가 유한한 단계에 실행을 끝마침 • 무대기이면 무잠금이다 • 기아(starvation)을 유발하기도 한다. • 성능을 위해 무대기 대신 무잠금을 선택하기도 한다.
  • 65.
    2-65 Lock없는 프로그램 • 넌블럭킹의 등급 – 제한된 무대기 (bounded wait-free) • 유한하지만 쓰레드의 개수에 비례한 유한일 수 있다. – 무간섭 (obstruction-free) • 한 개를 제외한 모든 쓰레드가 멈추었을 때, 멈추지 않은 쓰레드의 메소드가 유한한 단계에 종료할 때. • 충돌 중인 쓰레드를 잠깐 멈추게 할 필요가 있다.
  • 66.
    2-66 Lock없는 프로그램 • 정리 – Wait-free, Lock-free • Lock을 사용하지 않고 • 다른 쓰레드가 어떠한 행동을 하기를 기다리는 것 없이 • 자료구조의 접근을 Atomic하게 해주는 프로그램
  • 67.
    2-67 병행성과 정확성 • 그러면, Atomic Memory로 그런 자료구조를 만들면 되지 않는가? • Atomic Memory만으로는 다중 쓰레드 무대기 큐를 만들 수 없다!!!!!! – (증명) : 아까 그 책
  • 68.
    2-68 병행성과 정확성 • 다중 쓰레드 무대기 큐를 만들려면? – 합의 (Consensus)객체가 필요하다. • 합의 – 여러 개의 쓰레드 중 하나만 골라내어 그 선택자를 모두에게 알려주는 API – 당연히 무대기 – 실제 컴퓨터에 존재하는가? • 존재한다. • 특별한 CPU 명령으로 구현된다.
  • 69.
    2-69 합의(Consensus) • <넘어가자> • 동기화 연산의 능력을 알아보는데 사용되는 알고리즘 • 합의 객체 의 정의 <PASS> – value_t decide(value_t value) 메소드를 구현 – n 개의 스레드가 decide를 호출한다. – 하나의 스레드는 한번 이하로만 호출한다. – decide는 모든 호출에 대해 같은 값을 반환한다. – decde가 반환하는 값은 어떠한 스레드가 입력한 값이다.
  • 70.
    2-70 합의(Consensus) • 구현 (Windows API) – InterlockedCompareExchange() LONG __cdecl InterlockedCompareExchange( __inout LONG volatile *Destination, __in LONG Exchange, __in LONG Comparand ); – Destination에 저장된 값과 Comparand가 같은 값이면 Destination에 Exchange를 쓴다. – Destination에 저장되어 있던 값을 리턴한다. – Atomic하다.
  • 71.
    2-71 합의(Consensus) • 실제 x86 CPU상의 구현 – lock prefix와 cmpxchg 명령어로 구현 – lock cmpxchg [A], b 기계어 명령으로 구현 • eax에 비교값, A에 주소, b에 넣을 값 • EnterCriticalSection()도 위와 같은 어셈블리를 사용해서 구현되어 있다. – 하지만 lock을 얻지 못하면 Windows kernel 호출크리.
  • 72.
    2-72 합의(Consensus) • 합의의 위용 – 모든 자료구조를 멀티쓰레드 무대기자료구조로 만들 수 있다. – 바꿔주는 프로그램이 있다. – STL도 OK!
  • 73.
    2-73 합의(Consensus) Response Apply(Invocation Invoc, int threadId) { Node *prefer = new Node(Invoc); while (prefer->seq == 0) { Node *before = tail.GetMax(head); LONG decide_prefer = reinterpret_cast<LONG>(prefer); Node *after = reinterpret_cast<Node*>( before->decideNext.Decide(decide_prefer) ); before->next = after; after->seq = before->seq + 1; head[threadId] = after; } SeqQueue myObject; Node *current = tail.next; while (current != prefer) { myObject.Apply(current->invoc); current = current->next; } return myObject.Apply(current->invoc);
  • 74.
    2-74 희망 • Happy End???? – NO • 합의를 가지고 자료구조를 <효율적인> 무대기나 무잠금으로 구현하는 것은 매우 어려운 일이다. – 구현은 어렵지 않으나 성능은 안습이다.
  • 75.
    성능 비교 • 네할렘 XEON, E5520, 2.3GHz, 2CPU (8 core) • STL의 queue를 무잠금, 무대기로 구현한 것과, CriticalSection으로 atomic하게 만든것의 성능 비교. – Test조건 : 16374번 Enqueue, Dequeue (결과는 mili second) – EnterCriticaSection()을 사용한 것은 테스트 데이터의 크기가 128배 – 따라서 50000배 성능 차이 (4개 thread의 경우) 쓰레드 갯수 1 2 4 8 16 무잠금 만능 5091 7445 6811 10004 6846 무대기 만능 5021 7257 7026 9170 6466 EnterCritical 222 238 219 254 257 • EnterCriticalSection을 사용해야 하는가? – No : 멀티쓰레드에서의 성능향상이 없다.
  • 76.
    2-76 희망 • 결론 – CPU가 제공하는 Consensus를 사용하면 모든 싱글쓰레드 알고리즘을 Lock-free한 멀티쓰레드 알고리즘으로 변환할 수 있다. • 현실 – 비효율 적이다.
  • 77.
    2-77 희망 • 대안 – 자료구조에 맞추어 최적화된 lock-free알고리즘을 일일이 개발해야 한다. • 멀티쓰레드 프로그램은 힘들다. => 연봉이 높다. • 다른 데서 구해 쓸 수도 있다. – Intel TBB, VS2010 PPL – 인터넷 – 하지만 범용적일 수록 성능이 떨어진다. 자신에게 딱 맞는 것을 만드는 것이 좋다.
  • 78.
    2-78 내용 • 도입 • 현실 • Welcome to the Hell. • 새로운 희망 • 우리의 나아갈 길 • 실제성능
  • 79.
    2-79 실제 성능 • 속도 비교 예제) – Set의 구현 • 중복 불가 • 정렬되어 있음 – Add, Remove, Contains 메소드 – 여러 가지 병렬화 기법 사용 • Course, Fine, Optimistic, Fine, Lock-Free – 인용) 아까 그 책
  • 80.
    2-80 실제 성능 • Course Set EnterCriticalSection(&lock); pred = &head; curr = pred->next; – 모든 메소드에 Locking while (curr->key < key) { pred = curr; curr = curr->next; } if (key == curr->key) { pred->next = curr->next; delete curr; LeaveCriticalSection(&lock); return true; } else { LeaveCriticalSection(&lock); return false; }
  • 81.
    2-81 실제 성능 • Fine Set head.Lock(); pred = &head; curr = pred->next; – 수정이 필요한 curr->Lock(); while (curr->key < key) { pred->Unlock(); Node만 Locking pred = curr; curr = curr->next; curr->Lock(); } if (curr->key == key) { pred->next = curr->next; curr->Unlock(); pred->Unlock(); return true; } curr->Unlock(); pred->Unlock(); return false;
  • 82.
    2-82 실제 성능 while (true) { • Optimistic Set Node *pred = &head; Node *curr = pred->next; while (curr->key < key) { – 수정이 필요한 pred = curr; curr = curr->next; Node검색을 } pred->Lock(); curr->Lock(); Locking없이 하고 if (validate(pred, curr)) { if (curr->key == key) { – Locking 후 확인 pred->next = curr->next; pred->Unlock(); curr->Unlock(); // delete curr; return true; } else { pred->Unlock(); curr->Unlock(); return false; } } pred->Unlock(); curr->Unlock(); }
  • 83.
    2-83 실제 성능 • Lazy Set while (true) { Node *pred = &head; Node *curr = head.next; while (curr->key < key) { – 지운 노드를 직접 pred = curr; curr = curr->next; 지우지 않고 } pred->Lock(); curr->Lock(); marking만 한다. if (validate(pred, curr)) { if (curr->key != key) { curr->Unlock(); pred->Unlock(); – Contain()에서 return false; } else { 잠금을 없앰 curr->marked = true; pred->next = curr->next; curr->Unlock(); pred->Unlock(); – Optimistic의 최적화 } return true; curr->Unlock(); pred->Unlock(); } curr->Unlock(); pred->Unlock(); }
  • 84.
    2-84 실제 성능 • Lock-free Set bool snip; while (true) { – Lock()사용안함 Window *window = find(&head, key); Node *pred = window->pred; – Next필드와 marked필드를 Node *curr = window->curr; 동시에 atomic하게 if (curr->key != key) return false; else { 업데이트 하는 것을 구현 Node *succ = GetReference(curr->next); – Pointer의 LSB를 marked로 snip = curr->attemptMark(succ, true); if (!snip) continue; 사용 pred->CompareAndSet(curr, succ, false, false); return true; } }
  • 85.
    2-85 실제 성능 • Lock-free Set – CompareAndSet의 구현 bool CompareAndSet(Node *old_node, Node *next_node, bool old_mark, bool next_mark) { DWORD old_addr = reinterpret_cast<DWORD>(old_node); if (old_mark) old_addr = old_addr | 0x1; else old_addr = old_addr & 0xfffffffe; DWORD next_addr = reinterpret_cast<DWORD>(next_node); if (next_mark) next_addr = next_addr | 0x1; else next_addr = next_addr & 0xfffffffe; int prev_addr = InterlockedCompareExchange(reinterpret_cast<long *>(&next), next_addr, old_addr); return (prev_addr == old_addr); }
  • 86.
    2-86 속도 비교 • Intel i7 920 (단위: 초) – 32767번 랜덤한 숫자 추가, 삭제 반복 쓰레드 개수 Course Fine Optimistic Lazy 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
  • 87.
    2-87 실제 성능 • 우리의 목적 – 고성능 – 쉬운 프로그래밍 • 번역하면 – 무대기 (wait-free) – 멀티쓰레드 자료구조 (Queue, Stack, List~~~)
  • 88.
    2-88 정리 • 지향하는 프로그래밍 스타일 – Lock을 사용한 프로그래밍 • Blocking • 느림 (몇 백배) – Atomic Memory를 사용한 프로그래밍 • 표현력이 떨어짐 • Queue도 만들지 못함 – 무대기 자료구조를 사용한 프로그래밍 • OK
  • 89.
    2-89 정리 • 무대기(wait-free) 자료구조를 사용한 프로그래밍 – 각각의 자료구조를 Lock을 사용하지 않고 구현해야 한다. – Lock은 사용하지 않지만 합의객체는 필요하다. (합의 객체 없이는 만들 수 없음이 증명되어 있다.)
  • 90.
    2-90 정리 • 멀티 스레드 프로그래밍은 공유 메모리를 사용해서 스레드간의 협업을 한다. • 공유메모리를 사용한 동기화는 사용하기 힘들다. – 일관성, 중간 값 – Atomic memory의 한계 • 공유 자료 구조를 만들어서 사용하는 것이 좋다. • 좋은 공유 자료 구조는 만들기 힘들다. – 무대기(wait free)알고리즘의 작성은 까다롭다. – 상용 라이브러리도 좋다. Intel TBB, VS2010 PPL(Parallel Patterns Library)등
  • 91.
    2-91 미래 • 그래도 멀티쓰레딩은 힘들다. – 멀티쓰레드 프로그래머 연봉이 높은 이유 • Core가 늘어나면 지금 까지의 방법도 한계 – lock-free. wait-free overhead증가 – interlocked operation overhead증가 • 예측 – Transactional Memory – 새로운 언어의 필요 • 예) Earlang, Haskell
  • 92.
    2-92 TIP • Nehalem으로 업그레이드 하라. – 좋다! QPI만세 – Opteron을 쓸지언정 절대 구형 XEON은 쓰지 말아라. • Lock을 쓸 때 Spinlock은 TTS을 사용하고 일정 회전 이후는 Sleep()으로 쓰레드를 재워라. – RWLock(Reader Write Lock)을 사용할 수 있으면 하라. • 수정 후에는 반드시 Testing – Lock이 허름한 무대기/무잠금보다 성능이 높은 경우가 많다. • 메모리관리는 따로 해라 – tsmalloc(thread cashing malloc)도 괜찮다. • 인텔 매뉴얼을 믿지 마라. – CPU 실행방식 변경가능, CPU가 Arm으로 바뀔 수도 있다. – 매뉴얼 믿고 쓰다가 뒤통수 (너무 고약하다) – 자주 바뀌는 매뉴얼 • Worst Case 튜닝 – 저부하 일때 CPU낭비 상관 없음 • Wait-free는 Lock-free에서 starvation을 제거한 것
  • 93.
    2-93 NEXT • 다음 강의(내년???) – Lock-free search : SKIP-LIST – 효율적인 LOCK : tts, back-off, spin-count 고찰 – Next Solution • Stateless 프로그래밍 언어 • Transactional Memory – ABA Problem, aka 효율적인 reference counting
  • 94.
    2-94 Q&A • 연락처 – nhjung@kpu.ac.kr – <제작중> http://wsl.kpu.ac.kr/~gameserver/index.html – 발표자료 : ftp://210.93.61.41 id:ndc21 passwd: <바람의나라> • 참고자료 – Herlihy, Shavit, “The Art of Multiprocesor Programming”, Morgan Kaufman, 2008 – 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
  • 95.
    2-95 광고 • 체계적으로 배우고 싶으신 분 – 한국산업기술대학교 게임공학과 대학원 – 회사를 설득해서 계약 체결 • JCE와 계약 경험(win-win) • 회사로 출장 강의 • 등록금 공짜 (정부지원) • 석사학위 취득