SpinLock이란?

Spin Lock 은 이름이 뜻하는대로, 만약 다른 스레드가 lock을 소유하고 있다면 그 lock이 반환될 때까지 계속 확인하며 기다리는 것이다.

Spinlock은 다음과 같은 특성을 갖는다.

  1. Lock을 얻을 수 없다면, 계속해서 Lock을 확인하며 얻을 때까지 기다린다. 이른바 바쁘게 기다리는 busy wating이다.
  2. 바쁘게 기다린다는 것은 무한 루프를 돌면서 최대한 다른 스레드에게 CPU를 양보하지 않는 것이다.
  3. Lock이 곧 사용가능해질 경우 컨택스트 스위치를 줄여 CPU의 부담을 덜어준다. 하지만, 만약 어떤 스레드가 Lock을 오랫동안 유지한다면 오히려 CPU 시간을 많이 소모할 가능성이 있다.
  4. 하나의 CPU나 하나의 코어만 있는 경우에는 유용하지 않다. 그 이유는 만약 다른 스레드가 Lock을 가지고 있고 그 스레드가 Lock을 풀어 주려면 싱글 CPU 시스템에서는 어차피 컨택스트 스위치가 일어나야하기 때문이다

 출처 : http://devnote.net/wiki/index.php/Spinlock

즉, 멀티코어CPU 환경에서 Lock을 소유하고 있는 시간이 매우 순간일 경우에 Critical Section이나 Mutex대신에 사용하면 더욱 좋은 성능을 기대 할 수 있다.


SpinLcok 구현

01.class SpinLock
02.{
03.private:
04.static size_t const ms_NonSleepLoopCount = 1000000;//Sleep을 사용하지 않고 Lock을 체크하는 갯수 설정
05.LONG volatile    m_lOwnThreadID;//현재 Lock을 소유한 thread id
06. 
07.public:
08.SpinLock():m_lOwnThreadID(0){}
09.~SpinLock(){}
10. 
11.void Lock()
12.{
13.LONG lThreadID = static_cast<LONG>(::GetCurrentThreadId());
14. 
15.size_t loop = 0;
16.LONG lCompareID = 0;
17.while ( lCompareID != ::InterlockedCompareExchange(&m_lOwnThreadID, lThreadID, lCompareID) )
18.{// Lock을 소유할 수 없는 경우에 Loop를 돌면서 계속해서 체크
19.if ( loop > ms_NonSleepLoopCount )
20.{
21.::Sleep(0);// Sleep
22.}
23.else
24.{
25.++loop;
26.}
27.}
28.}
29. 
30.void UnLock()
31.{
32.LONG lThreadID = static_cast<LONG>(::GetCurrentThreadId());
33.LONG Exchange = 0;
34.LONG lRet = ::InterlockedCompareExchange(&m_lOwnThreadID, Exchange, lThreadID);
35.assert( lThreadID == lRet );
36.}
37.};

Lock을 걸때는 ::InterlockedCompareExchange를 이용하여 현재 소유한 threadid가 없을경우 내threadid로 설정을 시도한다.
내 threadid로 설정을 하지 못하면 계속해서 loop를 돌면서 시도를 한다.
이 때, loop를 너무 오랜시간 동안 돌면 오히려 성능이 나빠질 수 있으니, 적절히 Sleep를 사용할 수 있도록 한다.
(loop 를 돌때마다 Sleep을 사용한다면 컨택스트 스위칭이 일어나 SpinLock이 무용지물이 될 가능성이 많다)

UnLock 에서는 소유한 threadid를 해제시켜주기만 하면 된다.

위의 소스에서는 Recursive Call을 할경우에 DeadLock이 걸린다.
Recursive Call을 지원하기 위해 아래와 같이 수정하였다.

01.class SpinLock
02.{
03.private:
04.static size_t const ms_NonSleepLoopCount = 1000000;//Sleep을 사용하지 않고 Lock을 체크하는 갯수 설정
05.LONG volatile    m_lOwnThreadID;//현재 Lock을 소유한 thread id
06.int m_iRecursiveLockCount;//Recursive Call을 한 횟수 기록
07. 
08.public:
09.SpinLock():m_lOwnThreadID(0), m_iRecursiveLockCount(0){}
10.~SpinLock(){}
11. 
12.void Lock()
13.{
14.LONG lThreadID = static_cast<LONG>(::GetCurrentThreadId());
15. 
16.// 현재 소유 threadID가 호출 threadID와 같으면 Recursive Call Count 만 증가
17.if ( m_lOwnThreadID == lThreadID )
18.{
19.++m_iRecursiveLockCount;
20.assert( m_iRecursiveLockCount > 0 );
21.return;
22.}
23. 
24.size_t loop = 0;
25.LONG lCompareID = 0;
26.while ( lCompareID != ::InterlockedCompareExchange(&m_lOwnThreadID, lThreadID, lCompareID) )
27.{// Lock을 소유할 수 없는 경우에 Loop를 돌면서 계속해서 체크
28.if ( loop > ms_NonSleepLoopCount )
29.{
30.::Sleep(0);// Sleep
31.}
32.else
33.{
34.++loop;
35.}
36.}
37.}
38. 
39.void UnLock()
40.{
41.LONG lThreadID = static_cast<LONG>(::GetCurrentThreadId());
42. 
43.// Recursive Call Count가 있으면 감소만 함
44.if ( m_iRecursiveLockCount )
45.{
46.assert(m_lOwnThreadID == lThreadID);
47.--m_iRecursiveLockCount;
48.return;
49.}
50. 
51.LONG Exchange = 0;
52.LONG lRet = ::InterlockedCompareExchange(&m_lOwnThreadID, Exchange, lThreadID);
53.assert( lThreadID == lRet );
54.}
55.};

성능 테스트

그럼 이제 실제로 Critical Section과 SpinLock의 성능 차이를 테스트 하였다.
(테스트 방법에 따라 결과가 달라질 수 있음)

  - 테스트 환경 : 듀얼코어 CPU
  - 방법 
    1) std::map<int,int>을 하나 생성해서 1~1000000까지 키를 입력
    2) 각 쓰레드는 1~1000000까지 루프를 돌면서 map에서 find실행 이때, find한번당 Lock, UnLock을 호출하며, 루프가 종료되는데 걸린 시간을 측정
          for ( int iFind=0; iFind<1000000; ++iFind)
         {
          // Lock
              std::map<int, int>::const_iterator itr = g_map.find(iFind);
          // UnLock
          }
    3) 쓰레드 3개를 만들어서 결과 측정


측정결과는 예상대로 Critical Section을 사용하는 것 보다 SpinLock을 사용하는 것이 성능향상이 있었으며,
성 능향상율은 평균 20%정도나 되었다.

criticalsection.png
< Critical Section을 사용했을 때>

spinlock.png
< SpinLock을 사용 했을 때 >


출처 : http://yonmy.com/xe/blog/646


+ Recent posts