2014년 8월 26일 화요일

Lock-free는 과연 빠른가? (3)

이전 글의 테스트 소스를 좀 더 실전 상황에 맞게 수정해 보았다.

먼저 아래와 같이 각 단위테스트마다 do_something 함수를 호출하여 일반 코드와 유사한 구조를 가지도록 하였다. 참고로 일반 상황에서는 간헐적으로 thread가 blocking 되거나 wait/sleep 상태에 돌입할 수 있는데, 이 테스트에서는 배제하였다. Lock-Free 로직을 통한 별렬성을 최대한 발휘할 수 있도록 고려한 것이다.
LONG gSomeData[1024];
static void do_something() {
for (int i = 0; i < 1000; i ++) {
gSomeData[i] += 1;
}
}
DWORD WINAPI test_lock_free(LPVOID param) {
int cntTest = (int)param;
for (int i = 0; i < TEST_LOOP; i++) {
volatile LONG* p = gSharedData;
for (int j = 0; j < cntTest; j++) {
::InterlockedIncrement(p);
p++;
}
do_something();
}
return 0;
}
또한, SpinLock 의 lock() 함수는 아래와 같이 단순화하였고,

void lock() { int cnt = cntSpin; while (!tryLock()) { if (-- cnt <= 0) { Sleep(1); cnt = cntSpin; } } }

SlowSpinLock 과 FastSpinLock 은 각각 아래와 같이 초기화하였다.
static SpinLock gSlowSpinLock(20); // cntSpin=20
static SpinLock gFastSpinLock(20000); // cntSpin=20000

테스트 결과는 아래와 같다.


=========== Threads = 1, Test loop = 640*1000 ===============
atomic_update | lock_free | slow_lock | fast_lock | crit_sect
      1            359         374         374         359
      2            375         374         359         374
      3            375         358         375         374
      4            375         374         359         374
      5            390         375         374         374


=========== Threads = 2, Test loop = 320*1000 ===============
atomic_update | lock_free | slow_lock | fast_lock | crit_sect
      1            250         359         359         265
      2            265         328         327         265
      3            266         358         375         265
      4            281         359         343         265
      5            281         327         359         281


=========== Threads = 4, Test loop = 160*1000 ===============
atomic_update | lock_free | slow_lock | fast_lock | crit_sect
      1            234         358         359         219
      2            249         343         359         203
      3            265         343         328         203
      4            249         359         359         218
      5            250         359         359         202


=========== Threads = 8, Test loop = 80*1000 ===============
atomic_update | lock_free | slow_lock | fast_lock | crit_sect
      1            219         343         343         219
      2            187         343         328         218
      3            187         343         344         218
      4            203         343         297         218
      5            187         343         328         234


=========== Threads = 16, Test loop = 40*1000 ===============
atomic_update | lock_free | slow_lock | fast_lock | crit_sect
      1            187         312         297         218
      2            203         312         265         218
      3            203         312         265         219
      4            203         296         281         218
      5            187         312         281         234


=========== Threads = 32, Test loop = 20*1000 ===============
atomic_update | lock_free | slow_lock | fast_lock | crit_sect
      1            203         296         250         234
      2            187         312         250         218
      3            203         296         234         219
      4            203         280         250         218
      5            203         297         234         234


=========== Threads = 64, Test loop = 10*1000 ===============
atomic_update | lock_free | slow_lock | fast_lock | crit_sect
      1            202         390         359         234
      2            203         452         359         250
      3            218         328         312         234
      4            203         343         312         234
      5            218         375         343         312

결론적으로 일반적인 상황에서 lock-free 와 critical-section 간의 차이는 거의 없으며, spin_lock의 효율성은 크지 않는 것으로 보인다.

한 편, 자바 코드의 실행결과는 어떻게 될까? 실은 그 의외의 결과 때문에 이 글을 올리게 되었다. 보시다시피 모든 실행결과가 C로 작성된 테스트코드보다 빠르다.
아래는 Linux 상에서 실행한 Java 코드 수행 결과이다. 굳이 리눅스 상에서 테스트를 한 이유는 pthread_mutex 테스트 결과가 매우 저조해서, 이를 내부적으로 사용하는 자바의 퍼포먼스를 직접 검증하기 위한 것이다. 편이를 위해 옆에 pthread_mutex 테스트 값을 추가하였다.


=========== Threads = 1, Test loop = 640*1000  ===============
atomic_update = 1 : java = 214   pthread = 1411
atomic_update = 2 : java = 214   pthread = 1402
atomic_update = 3 : java = 214   pthread = 1407
atomic_update = 4 : java = 216   pthread = 1402
atomic_update = 5 : java = 216   pthread = 1406
                                                               
                                                               
=========== Threads = 2, Test loop = 320*1000  ===============
atomic_update = 1 : java = 212   pthread = 2015
atomic_update = 2 : java = 214   pthread = 2176
atomic_update = 3 : java = 214   pthread = 1983
atomic_update = 4 : java = 213   pthread = 2079
atomic_update = 5 : java = 216   pthread = 2143
                                                               
                                                               
=========== Threads = 4, Test loop = 160*1000  ===============
atomic_update = 1 : java = 160   pthread = 2437
atomic_update = 2 : java = 172   pthread = 2455
atomic_update = 3 : java = 166   pthread = 2404
atomic_update = 4 : java = 172   pthread = 2415
atomic_update = 5 : java = 173   pthread = 2347
                                                               
                                                               
=========== Threads = 8, Test loop = 80*1000 ================
atomic_update = 1 : java = 158   pthread = 2498
atomic_update = 2 : java = 159   pthread = 2564
atomic_update = 3 : java = 153   pthread = 2518
atomic_update = 4 : java = 160   pthread = 2526
atomic_update = 5 : java = 156   pthread = 2438
                                                               
                                                               
=========== Threads = 16, Test loop = 40*1000  ===============
atomic_update = 1 : java = 169   pthread = 2634
atomic_update = 2 : java = 153   pthread = 2584
atomic_update = 3 : java = 159   pthread = 2543
atomic_update = 4 : java = 164   pthread = 2550
atomic_update = 5 : java = 161   pthread = 2536
                                                               
                                                               
=========== Threads = 32, Test loop = 20*1000  ===============
atomic_update = 1 : java = 164   pthread = 2650
atomic_update = 2 : java = 158   pthread = 2627
atomic_update = 3 : java = 163   pthread = 2483
atomic_update = 4 : java = 161   pthread = 2469
atomic_update = 5 : java = 163   pthread = 2535

                                               
=========== Threads = 64, Test loop = 10*1000 ===============
atomic_update = 1 : java = 164  pthread = 2668
atomic_update = 2 : java = 166  pthread = 2651
atomic_update = 3 : java = 164  pthread = 2711
atomic_update = 4 : java = 167  pthread = 2780
atomic_update = 5 : java = 163  pthread = 2741


테스트 결과, do_something 함수의 내부 루프가 100이하이고, 쓰레드 수가 32개 이하인 경우, 위에 사용된 spin-lock 코드가 가장 빠르며, 200~500 사이는 lock-free 가 가장 빠르다.
자바 코드는 내부 루프가 50 인 경우에도 쓰레드 수가 64개 이상이면 가장 빠르다. 이후 내부 루프가 800 이상이 되면 거의 모든 테스트 항목에서 Java 코드 수행 속도가 가장 빨라진다.
이는 단순히 Sleep()을 사용한 위의 spin-lock 코드보다, 자바 모니터의 락 검사 메카니즘과 Thread-scheduling이 훨씬 더 효율적임을 의미한다.

굳이 쓰레드간 충돌에 의해 스케쥴링이 발생해선 안되는 상황, 즉 병렬컴퓨팅이 필요한 상황이 아니더라도 변수 한 두개를 InterlockedXxx 함수를 써서 변경하는 것은 속도 여부를 떠나, 구현과 사후관리를 위해서도 권장할만한 사항일 것이다.
다만 그 이상의 복잡한 상황이라면 일단은 주저없이 CriticalSection과 java synchronized 를 쓰는 것이 바람직하다. 실행환경이 한정되고 고정적이지 않는 한, 시스템이 제공하는 동기화 API 이상의 퍼포먼스를 내는 것은 매우 어려울 뿐더러, 안정성도 심하게 떨어뜨린다. 관련 최적화는 시스템이 매우 안정화된 상태로 미루는 것이 맞을 것이다.

멀티쓰레드 쓰레드 코딩 시 가장 중요한 두 가지 이슈는 퍼포먼스와 안정성이라 하겠다. 위 테스트 결과가 관련 시스템 설계 및 구현 시에 도움이 되기를 바란다.

2014년 8월 22일 금요일

Lock-free는 과연 빠른가? (2)

수십개의 쓰레드가 1~n개의 변수를 각각 atomic하게 변경하는 함수를 매우 빈번하게 호출한다고 가정하자.
해당 함수를 lock-free 로 구현한 버젼이 있고, spin-lock 이나 mutex 로 구현한 버젼이 각각 있다. 어떤 함수가 더 빠를까? 게다가 쓰레드 수가 충분히 많다면? 그리고 C++ 로 구현한 lock-free 함수와 자바 언어로 구현한 함수를 비교하면?
아마도 많은 개발자들이 쉽게 아래와 같이 대답할 것 같다.
1) 병렬처리 기법 즉, Lock-Free를 이용한 함수가 더 빠를 것이다.
2) 쓰레드 수가 많아질수록 Lock-free 기법을 이용한 함수가 더 빠를 것이다.
3) 질문자 수준이 의심된다. 당연히 C언어 Lock-Free 가 월등히 빠를 것이다.

과연 그럴까?
결론부터 말하면 위의 세 개의 답은 모두 틀렸다.
아래는 테스트에 사용된 소스코드와 그 결과값이다.
물론, 아래 테스트 결과는 매우 제한된 조건에서만 유의미하다. 다만, 잘못된 상식을 벗어나나게 하는 용도로는 큰 효과가 있을 것이다.


include <Windows.h>
#include <stdio.h>

using namespace std;

CRITICAL_SECTION cs;

class SpinLock {
public:
    SpinLock(int cntSpin) {
        this->spin_lock = 0;
        this->cntSpin = cntSpin;
    }

    void lock() {
        int v;
        while (::InterlockedCompareExchange(&spin_lock, (LONG)&v, 0) != 0) {
            if (cntSpin <= 0) {
                ::Sleep(1);
            }
            else {
                for (int i = 0; i < cntSpin; i ++) {
                    _asm nop;
                }
            }
        }
    }

    void unlock() {
        spin_lock = 0;
    }

    class Auto {
    public:
        Auto(SpinLock* spinLock) {
            this->spinLock = spinLock; 
            spinLock->lock();
        }

        ~Auto(){
            spinLock->unlock();
        }

    private:
        SpinLock* spinLock;
    };

private:
    volatile LONG spin_lock;
    bool cntSpin;
};

static SpinLock gSlowSpinLock(0);
static SpinLock gFastSpinLock(1);
volatile LONG gSharedData[20] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
static int TEST_LOOP;

DWORD WINAPI test_slow_lock(LPVOID param) {
    int cntTest = (int) param;
    for (int i = 0; i < TEST_LOOP; i++) {
        SpinLock::Auto lock(&gSlowSpinLock);
        volatile LONG* p = (LONG*)gSharedData;
        for (int j = 0; j < cntTest; j++) {
            *p += 1;
            p++;
        }
    }
    return 0;
}

DWORD WINAPI test_fast_lock(LPVOID param) {
    int cntTest = (int) param;
    for (int i = 0; i < TEST_LOOP; i++) {
        SpinLock::Auto lock(&gFastSpinLock);
        volatile LONG* p = (LONG*)gSharedData;
        for (int j = 0; j < cntTest; j++) {
            *p += 1;
            p++;
        }
    }
    return 0;
}


DWORD WINAPI test_lock_free(LPVOID param) {
    int cntTest = (int)param;
    for (int i = 0; i < TEST_LOOP; i++) {
        volatile LONG* p = gSharedData;
        for (int j = 0; j < cntTest; j++) {
            ::InterlockedIncrement(p);
            p++;
        }
    }
    return 0;
}


DWORD WINAPI test_critical_section(LPVOID param) {
    int cntTest = (int) param;
    for (int i = 0; i < TEST_LOOP; i++) {
        ::EnterCriticalSection(&cs);
        volatile LONG* p = (LONG*)gSharedData;
        for (int j = 0; j < cntTest; j++) {
            *p += 1;
            p++;
        }
        ::LeaveCriticalSection(&cs);
    }
    return 0;
}

static const int MAX_THREADS = 32;
int doLockTest(int cntThread, LPTHREAD_START_ROUTINE test_fn, int cntAtomicUpdate) {

    HANDLE th[MAX_THREADS];
    memset((void*)gSharedData, 0, cntAtomicUpdate*sizeof(int));

    ULONGLONG t0 = ::GetTickCount64();
    for (int i = 0; i < cntThread; ++i)
        th[i] = CreateThread(NULL, NULL, test_fn, (void *)cntAtomicUpdate, NULL, NULL);

    WaitForMultipleObjects(cntThread, th, TRUE, INFINITE);
    ULONGLONG t1 = ::GetTickCount64();

    for (int i = 0; i < cntAtomicUpdate; ++i) {
        if (gSharedData[i] != TEST_LOOP * cntThread) {
            printf("!!! Invalid test function");
            exit(-1);
        }
    }

    return (int)(t1 - t0);
}

int _tmain(int argc, _TCHAR* argv[])
{
    ::InitializeCriticalSection(&cs);
    for (int cntThread = 1; cntThread <= MAX_THREADS; cntThread *= 2) {
        TEST_LOOP = 12800 / cntThread * 1000;
        printf("\n\n=========== Threads = %d, Test loop = %d*1000 ===============\n",
            cntThread, TEST_LOOP/1000);
        printf("atomic_update | lock_free | slow_spin | fast_spin | crit_sect \n");
        for (int cntAtomicUpdate = 1; cntAtomicUpdate <= 5; cntAtomicUpdate ++) { 
            int t1 = doLockTest(cntThread, test_lock_free, cntAtomicUpdate);
            int t2 = doLockTest(cntThread, test_slow_lock, cntAtomicUpdate);
            int t3 = doLockTest(cntThread, test_fast_lock, cntAtomicUpdate);
            int t4 = doLockTest(cntThread, test_critical_section, cntAtomicUpdate);
            printf("% 7d % 14d % 11d % 11d % 11d\n", cntAtomicUpdate, t1, t2, t3, t4);
        }
    }   
    return 0;
}


아래가 위 소스코드를 실행한 결과이다.




=========== Threads = 1, Test loop = 12800*1000 ===============
atomic_update | lock_free | slow_spin | fast_spin | crit_sect
      1            109         125         125         281
      2            203         124         141         265
      3            296         141         140         297
      4            374         140         141         296
      5            453         156         140         296


=========== Threads = 2, Test loop = 6400*1000 ===============
atomic_update | lock_free | slow_spin | fast_spin | crit_sect
      1            359         125         452         749
      2            609         140         437         733
      3            827         140         531         592
      4           1061         141         499         702
      5           1139         140         936         686


=========== Threads = 4, Test loop = 3200*1000 ===============
atomic_update | lock_free | slow_spin | fast_spin | crit_sect
      1            344         124        1014         874
      2            608         141        1014         874
      3            873         141        1076         593
      4           1107         141        1045         874
      5           1060         141        1310         624


=========== Threads = 8, Test loop = 1600*1000 ===============
atomic_update | lock_free | slow_spin | fast_spin | crit_sect
      1            312         125        1747         874
      2            546         125        1778         952
      3            795         141        1778         593
      4           1092         140        1794        1045
      5           1092         156        1935         593


=========== Threads = 16, Test loop = 800*1000 ===============
atomic_update | lock_free | slow_spin | fast_spin | crit_sect
      1            296         140        3573         920
      2            531         140        2793         936
      3            811         140        2808         593
      4           1076         141        3167        1060
      5           1092         156        3339         593


=========== Threads = 32, Test loop = 400*1000 ===============
atomic_update | lock_free | slow_spin | fast_spin | crit_sect
      1            296         156        5881         921
      2            530         156        4945         952
      3            811         156        4883         577
      4           1092         156        6209         998
      5           1108         172        7004         593



아마 위 결과를 보고 믿지 못하는 분도 계실지 모르겠다. 그런 경우 소스코드를 직접 실행해 보시기 바란다.
보다시피 쓰레드 수가 2 이상인 경우, 단 한 개의 변수를 변경할 때도 SlowSpinLock 은 다른 모든 방식보다 보다 2~3배 이상 빠르다. (첨고로, FastSpinLock 은 다른 모든 방식보다 매우 큰 차이로 느리다.)
이러한 차이는 SlowSpinLock 이 매우 효과적으로 쓰레드간 경쟁상태를 최소화하기 때문에 발생하는 것으로 판단된다. CriticalSection은 FastSpinLock을 일부 포함하고 있어서 어정쩡한 결과가 나오는 것이라면, SlowSpinLock 과 유사하게 구현된 java synchronized 는 어떤 결과가 나올까?
아래가 그 결과이다. Windows 용 JDK 1.6.3 에서 테스트한 것이다.
놀랍게도 Thread 수에 관계없이 atomic 하게 변경하는 변수가 2 또는 3개 이상이면 java synchronized 가 C++ 로 구현한 Lock-free 코드보다 더 빨라지기 시작한다.




=========== Threads = 1, Test loop = 12800*1000 ===============
atomic_update = 1 : java_synchronized = 181
atomic_update = 2 : java_synchronized = 181
atomic_update = 3 : java_synchronized = 179
atomic_update = 4 : java_synchronized = 180
atomic_update = 5 : java_synchronized = 184


=========== Threads = 2, Test loop = 6400*1000 ===============
atomic_update = 1 : java_synchronized = 414
atomic_update = 2 : java_synchronized = 572
atomic_update = 3 : java_synchronized = 482
atomic_update = 4 : java_synchronized = 472
atomic_update = 5 : java_synchronized = 426


=========== Threads = 4, Test loop = 3200*1000 ===============
atomic_update = 1 : java_synchronized = 576
atomic_update = 2 : java_synchronized = 555
atomic_update = 3 : java_synchronized = 559
atomic_update = 4 : java_synchronized = 490
atomic_update = 5 : java_synchronized = 513


=========== Threads = 8, Test loop = 1600*1000 ===============
atomic_update = 1 : java_synchronized = 493
atomic_update = 2 : java_synchronized = 501
atomic_update = 3 : java_synchronized = 586
atomic_update = 4 : java_synchronized = 526
atomic_update = 5 : java_synchronized = 530


=========== Threads = 16, Test loop = 800*1000 ===============
atomic_update = 1 : java_synchronized = 530
atomic_update = 2 : java_synchronized = 538
atomic_update = 3 : java_synchronized = 524
atomic_update = 4 : java_synchronized = 522
atomic_update = 5 : java_synchronized = 569


=========== Threads = 32, Test loop = 400*1000 ===============
atomic_update = 1 : java_synchronized = 574
atomic_update = 2 : java_synchronized = 571
atomic_update = 3 : java_synchronized = 563
atomic_update = 4 : java_synchronized = 557
atomic_update = 5 : java_synchronized = 568


과연 자바는 최강의 멀티쓰레드 플랫폼이라 불릴만하다.

참고로, pthread_mutex 를 사용하면 CriticalSection 보다 2~3배 느려진다. Linux에서 Java 버젼도 이런 영향을 받아선지 성능이 뚝 떨어지지만 그래도 pthread_mutex 보다는 2배 이상 빠르다.

결론은 멀티쓰레드 코딩 방식에는 절대 강자는 없으며,  병렬컴퓨팅이 명확히 필요한 경우가 아니라면 lock-free 에 집착할 필요가 없다는 것이다. 위 테스트 결과처럼, 때로는 매우 단순한 CAS 함수와 Sleep 함수 하나 만으로도 매우 탁월한 퍼포먼스를 얻어낼 수 있다. Thread 간 충돌이 빈번히 발생하는 상황에서 Thread 블로킹을 최소화하려면 Lock-Free가 필요하지만 오히려 Lock을 적절히 사용하여 Thread 간 충돌을 최소화하여 퍼포먼스를 더 높일 수 있는 방법도 존재하는 것이다. 즉, 때로는 Lock-free 가, 때로는 FastSpinLock 이, 때로는 CriticalSection 이 가장 빠를 때가 있으며, 그때 그때 상황에 맞춰 해결하여야 한다. 다만, 자바 프로그래머라면 이런 고민에서 완전히 해방감을 느껴도 무방하다. ^^

누군가 C++11 의 mutex 관련 테스트자료도 올려주시면 고맙겠는데...
참고로, 아래에 자바 소스 코드도 첨부합니다.



public class TestMain {
    static int gSharedData[] = new int[20];
    static int TEST_LOOP;
 
    static class JSync extends Thread {
        JSync(int c) {
            this.cntTest = c;
            start();
        }
        public void run() {
            for (int i = 0; i < TEST_LOOP; i++) {
                synchronized (gSharedData) {
                    for (int j = 0; j < cntTest; j++) {
                        gSharedData[j] ++;
                    }
                }
            }
        }
        int cntTest;
    }



    static final int MAX_THREADS = 32;
    static int doLockTest(int cntThread, int cntAtomicUpdate, boolean testSync) throws InterruptedException {
        Thread th[] = new Thread[MAX_THREADS];
        for (int i = 0; i < cntAtomicUpdate; i ++) {
            gSharedData[i] = 0;
        }

        long t0 = System.currentTimeMillis();
     
        for (int i = 0; i < cntThread; ++i) {
            th[i] = new JSync(cntAtomicUpdate);
        }
        for (int i = 0; i < cntThread; ++i) {
            th[i].join();
        }
     
        long t1 = System.currentTimeMillis();
     
        for (int i = 0; i < cntAtomicUpdate; ++i) {
            if (gSharedData[i] != TEST_LOOP * cntThread) {
                System.out.print("!!! Invalid test function");
                System.exit(-1);
            }
        }

        return (int)(t1 - t0);
    }
 
 
    public static void main(String[] args) {
     
        try {
            for (int cntThread = 1; cntThread <= MAX_THREADS; cntThread *= 2) {
                TEST_LOOP = 12800 / cntThread * 1000;
                System.out.print("\n\n=========== Threads = " + cntThread + ", Test loop = " + TEST_LOOP/1000 + "*1000 ===============\n");

                doLockTest(cntThread, 1, true);
                for (int cntAtomicUpdate = 1; cntAtomicUpdate <= 5; cntAtomicUpdate += 1) {
                    int t1 = doLockTest(cntThread, cntAtomicUpdate, true);
                    System.out.print("atomic_update = " + cntAtomicUpdate
                            + " : java_synchronized = " + t1 + "\n");
                }
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

2014년 8월 15일 금요일

Lock-Free 는 과연 빠른가?

아래의 테스트 코드로 Lock을 사용한 알고리즘과 LockFree 알고리즘의 성능을 간단히 비교해보았다.
아래 코드는 n개의 변수를 atomic하게 변경할 때, 두 방식의 퍼포먼스를 비교하기 위한 것이다. Lock-free 구현 시 2개 이상의 변수를 atomic 하게 변경하는 범용적인 알고리즘은 존재하지 않으므로, 각각 독립된 n개 변수를 변경하는 것으로 비교하였다.

아래 코드는 쓰레드 간 충돌이 발생하지 않는 상황에서 테스트되었다. 일반적인 멀티쓰레드 프로그램 내에서 동일변수(또는 집합)을 동시에 참조하는 경우는 극히 드물게 발생하므로 그로 인한 실질적인 성능 감소는 무시할 수 있기 때문이다.


// Test code

CRITICAL_SECTION cs;

void compare_lock_free(int cntTest) {
const int TEST_LOOP = 1000 * 10000;
 volatile LONG v[20] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };

ULONGLONG t0 = ::GetTickCount64();
for (int i = 0; i < TEST_LOOP; i ++) {
::EnterCriticalSection(&cs);
volatile LONG* p = v;
for (int j = 0; j < cntTest; j ++) {
_asm mov eax, p;
_asm inc [eax];
p ++;
}
::LeaveCriticalSection(&cs);
}
ULONGLONG t1 = ::GetTickCount64();
for (int i = 0; i < TEST_LOOP; i ++) {
volatile LONG* p = v;
for (int j = 0; j < cntTest; j ++) {
_asm mov eax, p;
_asm lock inc [eax];
p ++;
}
}
ULONGLONG t2 = ::GetTickCount64();
for (int i = 0; i < TEST_LOOP; i ++) {
volatile LONG* p = v;
for (int j = 0; j < cntTest; j ++) {
::InterlockedIncrement(p);
p++;
}
}
ULONGLONG t3 = ::GetTickCount64();
printf("test = %d : lock = %d, lock_free 1 = %d, lock_free 2 = %d\n", 
cntTest, (int)(t1-t0), (int)(t2-t1), (int)(t3-t2));
}


// 실행 결과.
test =  1 : lock = 202, lock_free 1 = 110, lock_free 2 = 78
test =  2 : lock = 234, lock_free 1 = 171, lock_free 2 = 172
test =  3 : lock = 218, lock_free 1 = 250, lock_free 2 = 234
test =  4 : lock = 234, lock_free 1 = 343, lock_free 2 = 281
test =  5 : lock = 234, lock_free 1 = 437, lock_free 2 = 358
test =  6 : lock = 234, lock_free 1 = 515, lock_free 2 = 421
test =  7 : lock = 250, lock_free 1 = 593, lock_free 2 = 499
test =  8 : lock = 250, lock_free 1 = 686, lock_free 2 = 562
test =  9 : lock = 265, lock_free 1 = 733, lock_free 2 = 624
test = 10 : lock = 281, lock_free 1 = 811, lock_free 2 = 702


보다시피 3번 이상 atomic 연산이 필요한 경우, CriticalSection을 사용한 알고리즘이 더욱 빨리 실행된다. 일부러 함께 비교한 InterlockedIncreement() 함수가 assembly 코드보다 더 효율적인 것으로 나오는 것도 참고할 필요가 있을 것이다. 일부러 assembly 쓸 필요가 없다는. ^^

BeginCriticalSection() 과 EnterCriticalSection() 내부에서 _asm lock 이 각각 1회 이상 사용되므로, 결국 _asm lock 호출 회수에 의해 성능차이가 발생하는 것이라 하겠다. 위 코드를 pthread mutex 를 사용하는 것으로 바꾸어도 결과는 마찬가지일 것이다. x86의 _asm lock 오퍼레이션은 수백 사이클 이상 소모되는 매우 고비용 명령어임을 위 결과로 확인할 수 있다.

단 하나의 변수만을 치환할 때 CriticalSection 과 lock-free 알고리즘의 차이는 100ms / 천만, 즉 10ns 정도 차이가 발생한다. 서버 성능에 따라 좀 더 차이가 있을 수는 있겠으나, 일반적인 상황에서 이 정도 차이는 무시해도 무방할 정도이다.

다음 글에서는 쓰레드간 충돌이 매우 빈번하게 발생하는 경우에 대해 다뤄보도록 하겠다.

2014년 6월 27일 금요일

Throw C++ Exception in Android signal handler

gcc option: -fexceptions -fnon-call-exceptions -lgnustl_static


sighandler.cpp

#include <asm/sigcontext.h>
struct ucontext {
unsigned long  uc_flags;
struct ucontext  *uc_link;
stack_t  uc_stack;
struct sigcontext uc_mcontext;
sigset_t  uc_sigmask; /* mask last for extensibility */
};

static void throwCppException() {
    throw 1;
}

static void sig_handler(int sig, siginfo_t *si, void *unused) {
ucontext* uc = (ucontext*)unused;
#ifdef __thumb__
uc->uc_mcontext.arm_lr = (uc->uc_mcontext.arm_pc + 2) | 1;
uc->uc_mcontext.arm_pc = (int)(void*)throwCppException;
#elif defined __arm__
uc->uc_mcontext.arm_lr = (uc->uc_mcontext.arm_pc + 4);
uc->uc_mcontext.arm_pc = (int)(void*)throwCppException;
#elif defined(__i386__) || defined(__x86_64__)
(!!! Not Implemented !!!)
#endif
// just return. do not throw in sig_handler directly;
return;
}

void catch_signal_test() {
    struct sigaction sa;
    struct sigaction newAction, orgAction;

    sigemptyset(&newAction.sa_mask);
    newAction.sa_sigaction = sig_handler;
    newAction.sa_flags = SA_RESTART | SA_SIGINFO;
    sigaction(SIGSEGV, &newAction, &orgAction);

    // for Test
    try {
         volatile int* null_p = 0;
         *null_p = 1;
    }
    catch (int a) {
         ALOGD("Exception catched!!");
    }
}

2014년 5월 7일 수요일

멀티쓰레드 꼭 필요한가?

멀티쓰레드란 CPU 자원을 최대한 활용하기 위한 가장 효과적인 수단이다.
OS 차원에서 특정 쓰레드가 Idle 상태에 들어감과 동시에 다른 쓰레드에게 CPU 활용을 양보함으로써, 여러 트랜젝션을 병렬적으로 동시에 처리하는 것이 멀티쓰레드 주요 목적이다.

간단한 파일 업로드 서버의 경우, 각 트랜젝션(또는 세션)마다 쓰레드를 생성하고, 그 내에서 소켓을 읽어 파일에 쓰는 단순한 반복문을 넣어 주는 것만으로 손쉽게 멀티쓰레드 서버를 구현할 수 있다.
만약, 동시 접속을 몇 천개 이하로 제한하는 경우, 이것은 실제로 가장 손쉽고 가장 효율적인 서버를 구성하는 방법이다. 이 정도 시스템 상에서는 20ms 이상 길게 CPU를 사용하는 영역이 없기 때문에 컨텍스트 스위칭 부담이 전혀 없이 CPU를 100% 활용하는 것이 가능하다.

그러나, 처리 로직이 좀 더 복잡하고, 동시 접속 수가 더 크게 늘어나면 컨텍스트 스위칭 부담도 발생하고, 무엇보다 쓰레드 컨텍스트(스택)와 내부 데이터 처리를 위한 메모리 부담도 증가한다. 이는 트랜젝션 처리 속도에 영향을 미쳐 동시 접속 수가 더 많이 증가하는 악순환에 급격히 빠져 버릴 수 있다.
이러한 심각한 상황 발생을 막기 위해서는 쓰레드의 최대 개수를 제한하고 일정 부분 비동기 IO를 활용하는 수 밖에 없다. Windows의 IOCP ThreadPool 이 가장 대표적인 예라고 할 수 있다.

그렇다면, 아예 처음부터 전체 시스템을 비동기 IO만으로 구현하고 프로세스를 여러 개 띄우는 것이 더 나을 수도 있을까?
비동기 IO 냐, 멀티쓰레드냐를 떠나서 하나의 서비스를 여러 개의 프로세스로 나누게 되면 아래와 같은 문제가 발생한다.

1. 중앙집중적 관리.
동일한 서비스를 여러 개의 프로세스에서 가동하면 중앙집중적인 관리가 어려워진다.
로깅과 서비스 모니터링, 서비스의 변경 및 재시동 등 서비스 제어뿐 아니라 부하분산 등 분산처리 시스템 구성도 복잡해진다.
매우 제한적인 경우를 제외하곤 서버 당 (액티브한) 서비스는 하나만 가동되도록 설계하는 것이 옳바른 선택일 것이다.


2. 데이터 공유. (Buffering & Caching)
단일 프로세스 내에서의 데이터 공유는 해당 데이터의 포인터만 넘겨주면 된다. 트리나 배열처럼 공유하는 데이터의 양이 많거나 크기가 크더라도 단지 해당 포인터 하나만 넘겨주면 된다.
그러나 다중 프로세스 환경에서 데이터를 공유하려면 그 내용을 모두 전달해 주어야만 한다. 인코딩, 디코딩의 부담도 크지만, 해당 데이터가 서로 동기화되어야 한다면 그 처리는 매우 복잡해지게 된다. 특히, 복잡한 처리 절차를 반복하지 않고 미리 처리된 이전 결과를 즉시 반환하는 'Caching' 기법 사용에 많은 제약이 발생하게 되는데, 이러한 부분은 퍼포먼스에 큰 영향을 미칠 수 있다.

결론적으로 다중 코어 CPU를 사용하는 경우, 비동기 IO 만을 사용하여 멀티쓰레드 시스템과 동일한 퍼포먼스를 내는 것은 일반적으로 가능하지도 않고 그런 시도 자체가 바람직하지도 않다.

근래엔 일반 스마트폰 기기들도 멀티 코어 CPU를 장착하고 있다. 이제는 단일 코어를 사용하는 컴퓨터는 거의 없는 셈이다. 즉, 퍼포먼스가 중요한 프로그램이라면, 멀티쓰레드 사용은 필수적인 셈이다. 멀티쓰레드만이 하나의 프로세스가 CPU 다중 코어를 모두 활용할 수 있는 유일한 방안이기 때문이다.
멀티쓰레드를 사용하더라도 시스템 가용성을 최대화하기 위해서는 부분적인 비동기 IO 사용 또한 필수적이다. 이 두 가지는 조화롭게 사용되어야 한다.



2014년 3월 19일 수요일

Java 와 멀티쓰레드.


자바는 멀티쓰레드 코딩을 위해 만들어진 언어이다?
단지 "synchronized" 키워드 하나 추가한 것밖에 없는데 지나친 과장이 아니냐고 반문할 수 있다. 그러나 이는 멀티쓰레드 코딩의 편리성을 위해 자바가 얼마나 커다란 대가를 치르고 있는지 잘 모르기 때문일 것이다.

애초에 자바는 Process 가 지원되지 않는 저급한 RTOS 상에서 실행될 수 있도록 설계되었고, 프로세스 대신에 쓰레드 단위로 어플리케이션을 관리할 수 있어야만 했다.

이에, 자바 플랫폼에 내장된 기본 클래스(SDK)들이 멀티쓰레드 환경에서도 사용 가능할 수 있도록 다음과 같이 파격적인 설계를 가지게 되었다.

1) Java 의 모든 객체는 동기화 가능하다.
모든 자바 객체는 최소 4byte이상의 숨겨진 동기화구조체(Monitor)를 내부에 가지고 있다. (참고로 모니터는 mutex 와 condvar 를 합쳐 놓은 것과 같다.).
JVM의 메모리가 꽉 차서 GC가 발생하는 상황을 생각해보자. 자바는 모든 객체를 Heap 에 할당하기 때문에, 큰 어레이를 제외하곤 일반적인 객체의 평균 데이터 크기는 수십 byte 에 불과하다.
프로그램마다 실행 시의 평균 객체 크기는 다양하겠지만 40 byte 라면 무려 메모리의 10%, 80 byte라고 해도 5% 이상 동기화구조체를 위해 사용되는 것이다.
실로 엄청난 양이 아닐 수 없다.

2) 자바가 제공하는 대부분의 기본 클래스는 자체적으로 동기화를 처리한다. 
예전에 피쳐폰용 자바 플랫폼(MID)의 화면 출력용 클래스인 Canvas  함수들의 동기화 처리 속도를 분석해 본 적이 있다. 특정 데모 프로그램의 경우, 단지 20개 정도 함수의 동기화 처리 여부에 따라 무려 20%나 속도 차이가 발생했다.
그 외 일반 어플들의 profile 통계를 개인적으로 집계해 본 결과 CPU 타임의 평균 15% 정도가 동기화 처리에 사용되고 있었다.
당시 MIDP프로그램들은 대부분 단일 쓰레드 프로그램이었다. 그럼에도 불구하고, 약 5~10%의 메모리와 15% 정도의 CPU 타임을 동기화 처리에 사용하고 있었다.
(참고로, 근래에 자바의 기본 Collection 클래스들의 함수는 동기화처리가 생략되어 있다. 대신 별도의 Wrapper 클래스를 통해 동기화처리를 제공하도록 변경되었으므로 위의 통계치는 다소 달라졌을 것이다.)
가용 메모리가 줄어든 만큼 GC도 더욱 자주 발생해서 퍼포먼스 문제는 가중될 수밖에 없다.
Java 언어 설계 당시 모든 클래스에 동기화 기능을 넣는 문제로 관련자들이 얼마나 심사숙고했을지 능히 짐작할 수 있는 부분이다.

3) 가장 단순하고 쉬운 동기화 방식을 지원한다.
최근에는 세마포어나 AtomicValue 뿐 아니라 병렬처리용 클래스 들도 자바 플랫폼에 많이 추가되었다. 그러나 이들 클래스는 일종의 확장 utility에 해당하고, 여전히 자바가 제공하는 기본적인 동기화 방법은 synchronized 키워드를 사용하는 것이다.
특정 함수 또는 특정 block 을 synchronized 영역으로 선언하면, JVM 내부에서 동기화를 위한 lock 과 unlock 을 자동으로 처리한다. 즉, 프로그래머의 실수에 의해 동기화 설정 및 해제가 잘못될 가능성이 전혀 없다.
이 외에 최상위 클래스인 Object 에 추가된 동기화 관련 API 5개와, Thread 클래스에 속한 10여개 API가 자바로 멀티쓰레드 코딩을 하기 위해 알아야 할 전부이다. 더 이상 없다. 이 얼마나 단순한가!
물론, 해당 기능 정도는 C++ 로도 충분히 동일하게 구현할 수 있고 상황에 맞춰 훨씬 더 최적화할 수 있을 것이다. 그렇다면 자바는 메모리와 속도 감소를 감수하고, 단지 동기화 관련 API 를 좀 더 쉽게 쓸 수 있게 한 것에 불과할까?
메모리와 속도 문제는 애초 자바의 출발점이었던 임베디드 환경에서는 훨씬 더 치명적인 문제이다. 그런 큰 희생은 쉽고 단순함 그 이상의 의미를 위한 것일 것이다. 그건 바로 자바를 최상의 멀티쓰레드 개발 플랫폼으로 만들기 위한 것이었음을 이어서 이야기하고자 한다.


4) Java 는 클래스 단위로 내부 데이터를 동기화한다.자바의 제공하는 모든 기본 클래스는 그 내부 데이터 동기화를 스스로 처리한다. 또한 이 원칙은 어플리케이션 개발 시에도 그대로 적용될 수 있다. 모든 객체가 동기화 처리가 가능한 구조이기 때문이다. 이는 멀티 쓰레드 시스템 개발시 매우 큰 차이를 만들어낸다.

이런 동기화 기능을 내장하지 않는 언어 사용시에는 각 객체를 사용할 때마다 동기화처리를 각각 해주어야만 하며, 그 중 단 하나라도 실수한다면 해당 프로그램은 정상적인 수행이 불가능해진다. C/C++ 코딩 시 자바처럼 객체 단위로 동기화 범위를 잘게 나누어 많은 수의 동기화 객체를 다루는 것은 거의 불가능에 가까운 난해한 코드가 될 것이다.

좁은 의미에서 멀티쓰레드 프로그램 개발 용이성이란, 단일 쓰레드용으로 개발된 코드를 멀티쓰레드용으로 변경하는 것이 얼마나 쉬운가를 나타낸다고 할 수 있다.

C/C++ 의 경우, 이 문제는 정말 난해한 것이다. 구조체를 다루는 거의 모든 함수가 동기화 처리가 되어 있지 않을 뿐 아니라, 어떻게 동기화 기능을 넣을지 암담할 것이다. 특히 다른 쓰레드와 공유된 객체의 메모리 해제 시점을 정확히 정하는 것도 큰 문제가 된다. 이에 제한된 코드 영역을 따로 나누어 쓰레드화하게 되는데, 만일 해당 쓰레드가 사용하는 코드와 구조체가 명확하고 양이 적지 않다면 엄두를 낼 수 없는 일이 되고 만다.

자바의 경우, 기본 클래스는 이미 동기화 처리가 되어 있고, 새로 추가한 클래스들도 함수명 앞에 synchronized 란 키워드만 추가하여 쓰레드 동기화 문제를 쉽게 처리할 수 있다. 정확히 말해 그 이상 할 수 있는 것이 아예 없다. 또한, 특별히 메모리 관리에 따로 신경 쓸 필요도 전혀 없다.
단지 멀티 쓰레드 환경에 맞게 기존 코드를 변경하는 문제만 비교한다면 자바와 필적할 만한 다른 플랫폼을 찾기 어려울 것이다.

실제로 멀티쓰레드 프로그램 개발이 어려운 이유는 동기화 API 사용법 때문이 아니라 디버깅이 어려운 것이 주 요인이다. 관련 버그는 재현도 어렵고, 추적도 매우 어렵다.

자바의 단순함이 빛을 발하는 것은 이때이다. 소스 코드만 읽어서 멀티쓰레드 관련 버그를 찾아내는 것이 다른 언어보다 월등하게 쉬운 것이 자바이다. 그것은 관련 구조의 간명함 때문에 가능해진다.

5) 결론: 자바는 쉬운 멀티쓰레드 코딩을 위해 만들어진 언어이자 플래폼이다.
병렬 컴퓨팅이란 예외적인 연구 분야를 제외하면, 일반적인 멀티쓰레드 코딩이란 펑펑 노는 CPU 자원을 효율적으로 나눠쓰도록 코딩하는 것을 말한다.
대부분의 인터넷 서버가 이에 해당하는데, DB 서버 연결을 포함한 네트웍 통신 속도나 하드디스크 IO속도가 CPU 보다 수백만배 이상 느리다. 이런  서버 환경에서는 자바의 실행 속도가 전혀 문제되지 않는다. 한 트랜젝션을 수행하는 동안 실제 CPU 동작 시간은 1~2%도 채 되지 않으므로 멀티쓰레드를 적절히 사용하여 서버 가용성을 높히는 것이 중요하기 때문이다.

이러한 멀티쓰레드 아키텍쳐 설계 시 반드시 필요한 두 가지 구조체가 있는데, 그 둘은 버퍼와 큐이다.
버퍼는 실행 처리 속도가 다른 두 쓰레드 간에 데이터를 교환하기 위한 수단이고, 큐는 서로 개수가 다른 입력 쓰레드들과 출력 쓰레드들간에 m:n 방식의 태스크 데이터를 공유하는 방식이다.
버퍼는 단일 쓰레드 환경에서도 사용되는 범용적인 것이지만 큐는 멀티쓰레드 환경에 최적화된 시스템에서 주로 사용된다. 쓰레드 수가 어느 한계선 이상 많아지면 그 자체만으로 컨텍스트 스위칭 부하가 더 커지고, 동기화 충돌이 자주 일어나 오히려 처리 속도가 더 감소하는 문제가 발생할 수 있다. 쓰레드 풀과 큐를 이용하면 이러한 문제를 적절히 처리할 수 있다.

큐나 버퍼에 넣을 데이터 생성 및 처리 시간이 매우 짧지 않는 한, 데이터 전달 시의 동기화 처리 속도는 문제가 되지 않는다. 결과적으로 멀티쓰레드 시스템의 효율성은 동기화 관련 API 의 속도가 아니라 관련 아키텍쳐의 효율성에 의해 크게 좌우되게 된다.

즉, 멀티쓰레드 코딩의 편리성은 아키텍쳐 변경의 용이성과 밀접한 연관성을 가진다.

Java 는 어떤 언어보다 리팩토링이 쉽다. 자바의 동기화는 늘 객체 단위로 처리되며 한 함수 또는 한 함수 내의 일정 영역 내에서만 이루어지기 때문이다. 이러한 단순함이 다소 불편할 수도 있으나 대신에 디버깅과 리펙토링 부분에서 큰 보상을 얻을 수 있다.

C/C++로 작성된 멀티쓰레드 서버를 예를 들어보자. 그러저럭 베타 수준 작업이 된 상태에서 누군가 아키텍쳐 개선 얘기를 꺼낸다면 다른 개발자의 반응이 어떨까? 이것이 가장 핵심적이고 심각한 문제이다. 만약 매우 중요한 제안이었음에도 불구하고 단지 위험을 회피하고, 그대로 출시하였다면, 결국 그 상태에서 코드에 더께가 계속 쌓여 나중엔 조금만 수정해도 문제가 발생하는 최악의 상황으로 치달을 수 있다.

단지 멀티쓰레드 코딩이 쉽다고해서 해당 언어가 인터넷 서버 개발에 적합하다고 말하는 것은 성급할 것이다. 그러나 근래엔 어떠한 서버 프로그램이든 그 서비스가 성공적으로 운영되는 시점에는 거의 필수적으로 멀티쓰레드 아키텍쳐를 필요로 하게 되었다. 만약 선호도나 숙련도의 문제를 떠나 여러 언어 중에 하나를 서버 개발용으로 선택하고자 한다면, 개인적으로는 자바를 강권하고 싶다.
개인적인 좁은 경험 내에서 아래의 내용은 사실이기 때문이다.
“멀티쓰레드를 많이 사용하는 인터넷 서버는 자바 언어를 쓸 때 개발 속도가 빠르며, 그 성능도 더 빠르다”.

2014년 3월 10일 월요일

격리된 쓰레드 메모리 모델 (1)

- 들어가는 말 : Thread-Safety 와 Transaction

Thread-Safety 란 상호 연관된 두 개 이상 데이터를 참조하거나 변경하는 일련의 코드를 다중 쓰레드 환경에서 수행할 때, 관련 데이터간의 연관성을 정확히 유지할 수 있는가를 가리킨다.


한 쓰레드가 일련의 데이터 변경을 마치기 전에 다른 쓰레드가 일부 데이터를 변경한다면 데이터간 연관성을 유지할 수 없다. 이런 문제는 특정 함수를 통해서만 해당 데이터들의 변경이 가능하도록 강제하고, 해당 함수 내에서 적절한 동기화(synchronization) 처리를 함으로써 쉽게 해결할 수 있다.


그러나 데이터 참조 시에는 위와 같이 쉽게 해결되지 않는다. 데이터에 대한 참조는 읽어오는 과정만이 아니라 그 사용이 끝나야 종료되기 때문이다. 이로 인해 큰 크기의 Vector 형 객체나 Tree 형 객체의 내부 데이터를 안전하게 사용하기 위해선 긴 시간의 동기화가 필요하다. 긴 시간의 동기화는 퍼포먼스 감소 및 내부의 다른 동기화 과정과 얽혀 쓰레드 데드락이 발생하는 원인이 되기도 한다. 이런 경우 비동기 처리방식을 주로 사용하게 되는데, 도중에 데이터 변경이 발생하더라도 처리의 정합성을 보장하는 것은 까다로운 작업에 속한다.


이렇듯 Thread-Safety 문제는 데이터를 읽고 쓰는 과정만이 아니라, 연관성을 가진 일련의 데이터를 처리하는 전체 과정으로 확대된다. 입력된 데이터와 그 결과물의 정합성이 필요한 데이터 처리 단위를 Transaction 이라 한다면, Thread-Safety 란 다중 쓰레드 환경에서 특정 Transaction 수행의 안정성을 의미하게 된다.


아래에 소개하고자 하는 Isolated Thread Memory Model (ITM : 격리된 쓰레드 메모리 모델) 은 위에 언급된 Transaction 단위의 Thread-Safety 를 용이하게 확보하는 방법에 대한 개인적 아이디어를 정리해 본 것이다.




- Isolated Thread Memory Model (ITM)

ITM 은 아래의 규칙을 가진 개발 방법론과 그 적용 방법에 관한 것이다. 
  1. Thread-Safe 함수는 그 내부에서 Thread-Safe Access 만이 허용된다.
  2. Thread-Safe Access 는 아래와 같으며, Thread-Safety 를 보장한다. - 다른 Thread 에 의해 변경될 수 없는 메모리의 참조 및 변경 - Thread-Safe 함수 호출.
  3. thread_managed 함수는 규칙 1의 제약을 받지 않는 예외적인 Thread-Safe 함수이다. - 즉, Thread-Safe 함수에 의해 호출될 수 있다. - 내부 구현시 Thread-Safety 문제를 스스로 해결하여야 한다.
  4. 트랜젝션 관리 클래스 내부에 한해 thread_managed 함수를 추가할 수 있다. 
  5. 트랜젝션 관리 클래스의 객체를 Thread-Safe 함수의 인자로 전달할 수 있다.
위의 규칙을 이용하면 Thread-Safe 한 코드 영역과 그렇지 않은 영역을 명확하게 나눌수 있다. Thread-safety 와 관련된 문제를 트랜젝션 관리 클래스 내로 한정하여 집중시킴으로써 분석 및 처리의 효율성을 높히게 된다. 아울러 트랜젝션 단위의 Thread Safety 처리 또한 트랜젝션 해당 클래스 내부로 한정해서 처리하는 것이 가능해진다.
그러나, 일반적인 언어로 코딩 시엔  위의 규칙 2항을 개발자가 직관적으로 판단하여 준수하는 것이 불가능하다. 이에, 위의 방법론을 적용하기 위해선 특정 함수가 위의 규칙을 만족하는지 즉, Thread-Safety 를 보장하는지 여부를 증빙할 수 있는 자동화된 도구가 필수적으로 필요하다.
아래에 C++, Java 등의 OOP 언어에 몇 가지 문법적 요소와 특별한 클래스를 추가함으로써 Thread-Safety 여부를 컴파일 단계에서 명확히 확인할 수 있는 방법을 제시해 보았다.  
아래 내용은 초안이며, 사용된 용어나 정의들은 이후 계속 수정될 수 있음을 미리 밝혀둔다. 아래보다 더 좋은 규격안을 제시하거나 잘못되거나 부족한 점에 대한 지적은 기쁘게 환영하겠다.

- Special classes

아래 나열된 클래스들의 하위 클래스들은 아래와 같이 특별 처리한다.
  • StackLocal class - 해당 객체는 다른 쓰레드와 공유될 수 없다.
    - 해당 객체는 스택 내부에 생성된다. - 해당 객체는 항상 Thread-Safe 객체 상태를 갖는다. - StackLocal 객체 만이 StackLocal 객체를 필드로 가질 수 있다.
  • SynchronizedAtomic class - 해당 객체가 동기화되어 있는 동안 외부 쓰레드가 데이터를 변경할 수 없다.
    - 해당 객체가 동기화되어 있는 동안 Thread-Safe 객체 상태를 갖는다.
    - java 의 SynchronizedCollection 이 이에 해당.
  • Immutable class - 해당 객체는 생성이 완료된 후, 내부 데이터 변경이 불가능하다. - java 의 String, Enum 이 이에 해당.

  • ITMTransaction class - thread_managed 함수를 가질 수 있다. - StackLocal 클래스의 하위 클래스이다. 즉 스택 내부에만 생성된다. - 중요한 특수 함수. protected: void cancelTransaction(Exception e) thread_managed; // Exception 에 의해 비정상 종료시 자동 호출된다. public: thread_safe<T> getSnapshot(T*) thread_managed; // 주어진 객체의 복사본을 반환한다. protected: thread_safe<T> guaranteeThreadSafe(T*) thread_managed; // 주어진 포인터를 thread_safe 포인터로 변환한다. protected: thread_safe<T> lock(SynchronizedAtomic*) thread_managed; // 주어진 객체를 동기화 상태로 만들고 thread_safe 포인터를 반환한다. protected: void lock(SynchronizedAtomic*) thread_managed; // 주어진 객체를 동기화 상태를 해제한다.


- Thread-Safe 객체 상태 변경과 ITMTransaction

특정 객체가 외부 쓰레드에 의한 데이터 변경이 불가능한 상태에 있는 경우 해당 객체를 Thread-Safe 객체라 부른다.
특정 객체의 포인터 값을 다른 쓰레드와 공유된 메모리에 기록하는 순간 해당 객체는 Thread-Safe 상태를 잃게 된다.  
Thread-Safe 하지 않은 객체를 Thread-Safe 상태로 변경하는 가장 바람직한 방법은 getSnapshot() 함수를 이용해 해당 객체의 복사본을 얻는 것이다. 새로운 복사본의 변경 사항은 트랜젝션이 성공적으로 종료한 경우에 한해서 실제 메모리에 반영되므로 Thread-Safety 문제를 발생시키지 않는다. (이에 대해서는 다음 글에 설명).
특정 객체가 동기화(synchronized)된 상태일 때 그 데이터 변경이 불가능하다면, 해당 상태 동안 Thread-Safe 객체로 취급되며, 명시적 변환없이 해당 포인터를 thread_safe 포인터로 사용할 수 있다. 이를 사용하기 위해선 Java 의 synchronized 키워드처럼 동기화 영역을 명확히 설정할 수 있는 방법이 다른 언어에도 필요하다.
단, Thread-Safe 함수 내에서는 동기화 처리가 금지된다. 해당 함수를 잠깐 수행하는 동안의 동기화 처리는 문제되지 않더라도, 전체 트랙젝션 단위에서 볼 때는 Thread-Safe 하지 않을 수 있기 때문이다. 트랜젝션 단위의 긴 동기화는 lock(), unlock() 함수를 통해 처리한다. 참고로, Exception 발생시 unlock 처리는 자동으로 이루어진다.
또한, 특정 객체에 대한 포인터가 다른 쓰레드와 공유된 상태이기는 하나, 해당 객체에 대한 독점적인 변경 권한을 가지고 있으며, 내부 데이터 변경시 동기화가 필요없는 것을 확증할 수 있을 때는 해당 객체를 Thread-Safe 상태로 변경하여도 무방하다. 이런 경우 guaranteeThreadSafe() 함수를 사용한다.  
(이러한 강제 변환 시에는 Snapshot 이 원본 객체와 동일하게 생성된다.만약 미리
만들어진 Snapshot 이 존재하는 경우에는 오류가 발생한다)


- Thread-Safety Type 선언

변수, 필드, 함수 선언시 아래와 같이 Thread-Safety 유형을 선언한다.
  • thread_safe 포인터 ex) thread_safe <Foo> foo; - Thread-Safe 객체만을 가리키는 포인터이다. - 필드 선언 시에는 StackLocal 유형 클래스 내에서만 사용 가능하다.
  • thread_unsafe 포인터) - Thread-Safety 유형이 명시되지 않은 일반 포인터를 따로 구분하기 위하여 thread_unsafe 포인터라 칭한다.
    - 해당 포인터가 가르키는 객체의 Thread-Safe 상태를 확인할 수 없음을 의미한다.
  • local 함수 - ex) Foo* getValue() local; - Thread-Safe 객체 상태인 경우에 한해, Thread-Safety 가 보장되는 함수이다. - Thread-Safe-Access 외에 추가로 내부 데이터의 참조/변경이 허용된다.
  • thread_safe 함수 - ex) int getType() thread_safe; - Thread Safe 객체 상태와 관계없이 Thread-Safety 를 보장하는 함수이다. - StackLocal 객체 외에는 내부 데이터의 참조/변경이 허용되지 않는다. 단, 변경 불가능한 데이터 참조는 허용된다.
  • local thread_safe 함수 - ex) void setValue(String* value) local thread_safe; - Thread-Safe 객체 상태에서만 호출 가능한 local 함수이다. - 내부 데이터 참조/변경 시에도 Thread-Safety가 보장된다.
  • thread_unsafe 함수 - ex) void setParent(Node* node, Node* parent) thread_unsafe; - thread_unsafe 를 명시하기 위해 사용된다.
  • thread_managed 함수 - ex) void setParent(Node* node, Node* parent) thread_managed; - Thread-Safety 문제 처리를 담당하는 함수이다. - ITMTransaction 및 그 하위 클래스 내에서만 선언가능하다.
  • 함수의 Thread Safety 기본값 - Immutable 유형 클래스 내 함수 : thread_safe - ITMTransaction 유형 클래스 내 함수 : thread_safe - 그 외 : thread_unsafe
  • 사용 예 class Buffer { char* data; int length; Buffer(int length) { data = new char[length]; this->length = length; } char* getBuffer() local; int getLength() local; int getType() thread_safe; void init(thread_safe<char> data, int len) local thread_safe; };

- thread_safe 포인터 속성 변환

  • StackLocal 객체에 대한 포인터는 명시적 선언이 없어도 항상 thread_safe 포인터로 취급된다.

  • thread_unsafe 포인터는 ITMTransaction 내부 함수를 통해서만 thread_safe 포인터로 전환될 수 있다.
  • thread_safe 포인터는 thread_unsafe 포인터로 변환될 수 있다.
  • new operator, clone 등 객체 생성용 함수는 thread_safe 포인터를 반환한다.



- 해설과 문제점.

이로써 기존 언어에 추가되어야 하는 문법적 요소와 규칙에 대한 정의는 마무리되었다.  
위 문법적 요소는 임의의 thread_safe 포인터가 가리키는 객체의 Thread-Safe 상태가 항상 유지됨을 보장하기 위한 것으로 요약될 수 있다.
위 문법은 소스 분석만으로 Thread-Safety를 증빙하기 위해선 필수적으로 필요하긴 하나 그로 인해 코드 개발에 있어 큰 제약을 가지게 되어 실용성에 문제를 안고 있다.
StackLocal 유형을 제외한 모든 클래스 필드는 thread_safe 포인터를 필드로 가질 수 없다. 이 때문에 거의 모든 함수가 thread_unsafe 포인터를 반환하게 된다. 실제로 특정 Thread-Safe 함수 내에서 ITMTransaction 객체를 사용하지 않았다면, 해당 함수는 -그 복잡성 여부를 떠나- 함수형 언어로 변환이 가능할 정도로 Thread-Safe 함수의 제약 조건은 가혹하다.
Thread-Safe 함수 내에서 공유된 객체의 Sanpshot 생성을 자동으로 처리해주면 Thread-Safe 함수의 구현이 상당히 용이해질 수 있다. 이러한 자동처리는 기존의 Transactional Memory Model 과 개념상 동등한 Transaction 단위의 메모리 관리 기술이 필요하게 된다. 다음 글에서는 이와 관련된 메모리 버젼 관리 시스템과 기존의 표준 라이브러리 활용 방안에 대해서도 다뤄보고자 한다.
그에 앞서 이번 글은 위의 방법론과 문법적 요소에 대해 객관적 검증의 기회를 얻고자 쓰여진 것이다. 이 글은 최소의 문법적 요소를 추가함으로써 특정 함수의 Thread-Safety를  검증하는 것이 가능하다는 것을 증명하는 것이 주목적이다. 이에 내부의 메모리 관리 방식 등 좀 복잡한 내용은 다음로 미루고자 한다.
이 글의 논리적 오류에 대한 구체적인 지적과 조언을 기다린다.
끝으로 이해를 돕고자 아래에 간단한 Java 예제 코드를 추가하였다.



- Java 코드 예제


class Result { ... } interface Inspector { void inspect(String content) thread_safe; }; class ConcurrentTransaction extends ITMTransaction { Vector targetUrls; thread_safe<Inspector> inspector;  thread_safe<Vector> results ;
ConcurrentTransaction(Vector searchUrls, Inspector inspector) { this.targetUrls = searchUrls; this.inspector = inspector.clone(); this.results = new Vector; } public thread_safe<Vector> runTask() local throws Exception { while (true) { String url = popData(); if (url == null) { return; } try { Result res = doTask(url); if (res != null) { results.add(res); } } catch (Exception e) { logDebugInfo(e); } } return results; } Result doTask(String url) thread_safe { // 특정 Web-Page 열어, 내용을 다운로드 받아 문자열 생성. return inspector.inspect(webContents); } String popData() thread_managed { synchronized (targetUrls) { int cntRemainTask = targetUrls.size(); if (cntRemainTask == 0) { return null; } String url = targetUrls.lastElement(); targetUrls.setSize(cntRemainTask - 1); return url; } } void addResult(Result res) thread_managed { results.add(res); } private void commitResultToDBMS() thread_managed { // 결과를 DB 에 기록. } // Exception 에 의한 비정상 종료 시 자동 호출된다. protected void cancelTransaction(Exception e) thread_managed { logDebugInfo(e); } private void logDebugInfo(Exception e) thread_managed { // 로그 기록 } }