InitializeCriticalSectionAndSpinCount, CriticalSection에 대한 정보 이것저것
개인적으로 만들고 있는 프로젝트, 프로그램들은 대부분 리눅스/윈도우간의 크로스플랫폼을 지향하고 있는데 그래서 그런지 락을 쓸 때는 std::mutex를 자주 쓰고 있다.
최근에는 윈도우의 critical section이 더 빠르다는 얘기가 자꾸 들려서 윈도우일 때는 critical section을 더 쓰도록 변경하는 중.
그러다가 InitializeCriticalSection 이라는 초기화함수 대신 InitializeCriticalSectionAndSpinCount 라는 함수를 보게 되었는데 이게 무엇인지 찾아보았다. 이 함수의 인자는 2개인데 첫번째는 크리티컬섹션 객체의 포인터이고 뒤에 붙는 인자는 스핀 횟수이다.
어떤 스레드가 작업을 하다가 크리티컬섹션에 걸려서 InitializeCriticalSection 을 썼다면 즉시 대기상태(커널 대기)에 들어가게 된다. InitializeCriticalSectionAndSpinCount 를 썼다면 바로 대기상태에 들어가지 않고 인자로 줬던 SpinCount 횟수만큼 유저 모드에서 짧게 도는(스핀) 시간을 갖는다. 만약 그 사이에 크리티컬섹션이 풀리면 바로 다시 작업을 진행하고, SpinCount 횟수만큼 기다렸는데도 풀리지 않았다면 그제서야 커널 대기상태로 들어간다.
멀티프로세서 시스템에서, 락이 아주 짧게만 걸려 있는 경우라면 커널 모드 전환(컨텍스트 스위칭)을 아예 건너뛰고 스핀만으로 락을 얻을 수 있어 성능에 도움이 된다. 반대로 락을 오래 쥐고 있는 코드라면 스핀 시간만 낭비하고 결국 대기 상태로 들어가므로 큰 의미가 없다. 싱글 프로세서 시스템에서는 SpinCount가 사실상 무시되고 0으로 처리된다. 다른 CPU 코어가 그동안 락을 풀어줄 수가 없기 때문이다.
CRITICAL_SECTION g_cs;
int main()
{
if ( !InitializeCriticalSectionAndSpinCount( &g_cs, 4000 ) )
{
// 초기화 실패 처리
}
...
}
2026년 수정 안내 원래 이 글에는 “
InitializeCriticalSection은 반환값이 void라 실패를 알 수 없고, 실패 시 예외가 발생해 추적하기 힘든 버그가 생긴다”는 우려가 적혀 있었다. 이 우려는 Windows Vista 이후로는 사실상 해소되었다. 그리고 지금은 애초에 크리티컬 섹션보다 SRWLOCK이 권장된다. 둘 다 정리한다.
InitializeCriticalSection의 실패 우려는 Vista 이후 해소됐다
원래 이 함수를 걱정했던 이유는, Windows 2000/XP 시절에는 크리티컬 섹션 내부에 쓰이는 이벤트 객체를 초기화 시점에 미리 만들었기 때문이다. 이 과정에서 메모리 부족이 발생하면 STATUS_NO_MEMORY 예외가 던져질 수 있었고, InitializeCriticalSection은 반환값이 void라 이걸 정상적으로 처리할 방법이 없었다.
Windows Vista부터는 이벤트 객체를 필요할 때(실제로 경합이 생겨서 대기해야 할 때)가 되어서야 지연 생성(lazy allocation)하도록 바뀌었다. 그 결과 InitializeCriticalSection 자체에서는 더 이상 이런 예외가 발생하지 않는다. 이 글을 쓴 2017년 시점에는 XP 지원이 이미 끝난 뒤였으니, 사실 이 우려는 이미 무의미해진 근거였다.
InitializeCriticalSectionAndSpinCount가 반환값을 갖는 것은 지금도 사실이지만, “실패를 알 수 있다”는 것 자체가 주된 채택 이유는 아니게 되었다. 지금 이 함수를 쓰는 이유는 순수하게 스핀 카운트를 지정하기 위해서다.
지금은 SRWLOCK이 권장된다
Windows Vista부터 Slim Reader/Writer Lock(SRWLOCK)이 추가되었고, Microsoft는 새 코드에 크리티컬 섹션보다 이쪽을 권장한다.
SRWLOCK g_lock = SRWLOCK_INIT; // 별도의 초기화 함수 호출이 필요 없다
void Worker()
{
AcquireSRWLockExclusive( &g_lock );
// ... 임계 영역 ...
ReleaseSRWLockExclusive( &g_lock );
}
크리티컬 섹션 대비 장점은 이렇다.
- 더 작다.
CRITICAL_SECTION은 40바이트 안팎이지만SRWLOCK은 포인터 하나 크기다. - 초기화/삭제 함수가 필요 없다.
SRWLOCK_INIT으로 정적 초기화가 가능하고, 삭제할 것도 없다. - 읽기/쓰기 락을 구분할 수 있다.
AcquireSRWLockShared로 여러 스레드가 동시에 읽기만 하도록 허용할 수 있다. 크리티컬 섹션에는 이 구분이 없다. - 재귀 진입이 안 된다. 크리티컬 섹션은 같은 스레드가 다시 잡을 수 있지만 SRWLOCK은 안 된다. 대신 이 제약 덕분에 내부 구조가 더 가볍다.
크로스플랫폼이 목표라면
크로스플랫폼을 지향한다고 적었는데, 그렇다면 오히려 std::mutex/std::shared_mutex를 계속 쓰는 편이 나을 수 있다. Windows용 표준 라이브러리 구현은 내부적으로 크리티컬 섹션이나 SRWLOCK을 그대로 활용하므로, 플랫폼 API를 직접 감싸는 것과 성능 차이가 크지 않다. 락 전환에 드는 코드 관리 비용 대비 이득이 크지 않다면, 표준 라이브러리를 유지하는 것도 합리적인 선택이다.
댓글 남기기