사용자 도구

사이트 도구


kb:threadsynchronization

Thread Synchronization

스레드 동기화 관련 내용들을 기록해 두기 위한 페이지

기본

  • 동기화 관련 시스템 콜들은 CPU를 꽤나 잡어먹기 때문에 최대한 피하는 것이 좋다.
  • 동기화 관련 객체 LOCK/UNLOCK 사이의 코드는 최대한 짧게 짜야한다.

Critical Section

왜 빠른가?

크리티컬 섹션이 유저 모드에서 동작하기 때문에 일반적으로 뮤텍스보다 빠르다(2 ~ 10배)는 것은 널리 알려져 있다. 왜 빠른지에 대해 약간 정리해보자면…

  • 스레드가 EnterCriticalSection 함수를 호출한 경우, 제일 먼저 CRITICAL_SECTION 구조체 내부의 변수를 검사하게 된다. 이 값이 off인 경우, 바로 atomic 연산을 통해 이 값을 on으로 바꾸고 다음 작업을 진행하게 된다.
  • 크리티컬 섹션이 다른 스레드에 점유되어 있는 경우, 싱글 CPU 상에서는 바로 커널 모드로 넘어가게 되고, 다중 CPU 상에서는 Spin Count만큼 대기(busy-waiting)하면서, 락이 풀리기를 기다리게 된다.
  • 커널 모드로 넘어가게 되면, 해당 스레드는 세마포어를 이용한 WAIT 상태가 된다.
  • LeaveCriticalSection 함수를 호출하게 되면, CRITICAL_SECTION 구조체 내부의 변수를 off로 바꾸고, 세마포어를 이용한 경우에는 기다리고 있는 스레드에게 통지를 해주게 된다.

정리해보자면 크리티컬 섹션이 뮤텍스보다 빠른 것은 2가지 경우다.

  • 같은 크리티컬 섹션 객체에 접근하는 스레드의 숫자가 적어서, CRITICAL_SECTION 내부 락 변수만으로 동기화가 이루어질 때.
  • 적당한 스핀 카운트를 통해 WAIT 상태로 가기 전에 락을 획득할 수 있을 때. (SMP 머신의 경우)

즉 크리티컬 섹션 관련 스레드가 최대한 커널 모드로 들어가지 않도록 해줘야한다는 말이다.

Critical Section Spin Count

위에서도 설명했듯이, SMP 시스템에서 크리티컬 섹션과 관련된 병목을 줄이기 위해서는 적당한 스핀 카운트를 설정해줘야한다. 커널 모드로 들어가기 보다는 유저 모드에서 busy-waiting을 하는 게 나을 수 있기 때문이다. 이를 위한 함수가 SetCriticalSectionSpinCount, InitializeCriticalSectionAndSpinCount 함수다. Spin Count 값으로서 어느 정도의 값을 주는지는 당연히 시스템/애플리케이션마다 틀리다.

Semaphore Throttles

하나의 동기화 객체(크리티컬 섹션 또는 뮤텍스)를 기다리는 스레드가 너무 많은 경우, 세마포어를 하나 더 둬서, 해당 동기화 객체를 기다리는 스레드의 숫자를 줄여주는 것이 성능 향상에 도움이 될 수 있다.

while (TRUE) { // Worker loop
   WaitForSingleObject (hThrottleSem, INFINITE);
   WaitForSingleObject (hMutex, INFINITE);
      ... Critical code section ...
   ReleaseMutex (hMutex);
   ReleaseSemaphore (hThrottleSem, 1, NULL);
} // End of worker loop

Conditional Variable Model

Spin-Lock

초간단 스핀락. 일반적(?)인 락에 비해 크기가 작고(4바이트!), 전역 변수로 사용하기가 쉽다. 대신 CPU를 혹사시킨다는 단점이 있다. -_-;

// 락의 상태
enum
{
    LOCKED   = 0, ///< 잠겼음
    UNLOCKED = 1, ///< 풀렸음
};
 
/// \brief 스핀락을 초기화한다.
long InitSpinLock()
{
    return UNLOCKED;
}
 
/// \brief 스핀락을 건다.
void EnterSpinLock(volatile long* target)
{
    for (;;)
    {
        if (InterlockedExchange(target, LOCKED) == UNLOCKED)
            break;
    }
}
 
/// \brief 스핀락을 푼다.
void LeaveSpinLock(volatile long* target)
{
    InterlockedExchange(target, UNLOCKED);
}

MCS(Mellor-Crummey and Scott) Spin-Lock

Algorithms for Scalable Synchronization on SharedMemory Multiprocessors (PDF)

기존의 스핀락 같은 경우, 같은 주소의 변수를 여러 스레드가 경쟁적으로 계속 액세스하므로, 버스 쪽에 걸리는 부하가 심하다.

MCS 스핀락은 락마다 큐를 두고, 스레드 B가 진입시 락을 획득하지 못했다면, 큐에다 자신의 로컬 변수에 대한 참조를 집어넣는다. 락을 이전에 획득한 스레드 A는 락을 풀면서, 큐의 제일 앞에 있는 변수를 변경한다. 그러면 기다리고 있던 스레드 B가 락을 획득하게 되는 것이다.

말로는 훌륭해보이나, CPU 갯수보다 많은 스레드가 액세스시 오히려 느려질 수도 있다. A 스레드가 락을 풀면서 B 스레드의 로컬 변수를 수정했는데, 마침 B 스레드가 OS 스케쥴러에 의해서 슬립(?) 상태였다면, 그 뒤에 있는 C, D, … 스레드들도 같이 기다리게 되기 때문이다. 이렇게 되면 전체적인 스로우풋이 오히려 떨어질 수 있다 하겠다. Spinlocks and Read-Write Locks 페이지에 가면 자세하게 기술해놨다. 리눅스긴 하지만, 이해하는데 별 문제는 없다.

링크


kb/threadsynchronization.txt · 마지막으로 수정됨: 2014/11/14 15:49 저자 excel96