SpinLock이란?
Spin Lock 은 이름이 뜻하는대로, 만약 다른 스레드가 lock을 소유하고 있다면 그 lock이 반환될 때까지 계속 확인하며 기다리는 것이다.
Spinlock은 다음과 같은 특성을 갖는다.
- Lock을 얻을 수 없다면, 계속해서 Lock을 확인하며 얻을 때까지 기다린다. 이른바 바쁘게 기다리는 busy wating이다.
- 바쁘게 기다린다는 것은 무한 루프를 돌면서 최대한 다른 스레드에게 CPU를 양보하지 않는 것이다.
- Lock이 곧 사용가능해질 경우 컨택스트 스위치를 줄여 CPU의 부담을 덜어준다. 하지만, 만약 어떤 스레드가 Lock을 오랫동안 유지한다면 오히려 CPU 시간을 많이 소모할 가능성이 있다.
- 하나의 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%정도나 되었다.
< Critical Section을 사용했을 때>
< SpinLock을 사용 했을 때 >
출처 : http://yonmy.com/xe/blog/646