CRITICAL_SECTION은 해당 struct의 데이터를 보면 알 수 있는 것처럼 다양한 기능이 있습니다. Windows Kits 10의 기준에 맞춰서 해당 자료 구조를 살펴보고 어떠한 기능이 있는지 한번 찾아보도록 하겠습니다.
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
//
// The following three fields control entering and exiting the critical
// section for the resource
//
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread; // from the thread's ClientId->UniqueThread
HANDLE LockSemaphore;
ULONG_PTR SpinCount; // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
ULONG_PTR SpinCount는 CRITICAL_SECTION의 스핀 카운트를 저장하는 변수로 보입니다. InitializeCriticalSectionAndSpinCount의 초기화 함수를 통해서 SpinCount의 값을 초기화 하는 시점에 변경할 수 있습니다. Windows Via C/C++ 책을 참조하면 SpinCount의 적절한 개수는 바로 ContextSwiching 단계로 넘어가지 않고 스핀락의 형태로 진입전에 대기하기 때문에 성능 향상을 기대할 수 있으며 대략 4500값 정도면은 좋은 성능을 기대할 수 있다고 합니다. 실제 코드를 실행해 보면서 값의 변화를 확인해보겠습니다.
InitializeCriticalSectionAndSpinCount로 초기화 시 SpinCount값이 확실히 변하는 것을 확인할 수 있습니다. 그리고 12th Gen Intel(R) Core(TM) i7-12700H 2.30 GHz 해당 CPU 기준으로 InitializeCriticalSection으로 초기화 할 경우에 33556432값으로 세팅되는 것을 확인했습니다
CRITICAL_SECTION은 Recursive Lock이 허용되는 Lock 개체 입니다. 이것이 가능한 이유는 LockCount를 별도로 기록해 놓기 때문인 것으로 보입니다. EnterCriticalSection 함수를 호출해서 데이터가 어떻게 변경되는지 파악 하겠습니다. 실험 코드는 아래와 같습니다.
int main()
{
CRITICAL_SECTION criticalSection1;
//if ( false == ::InitializeCriticalSectionAndSpinCount( &criticalSection1, 4500 ) )
{
::InitializeCriticalSection( &criticalSection1 );
}
::EnterCriticalSection( &criticalSection1 );
::EnterCriticalSection( &criticalSection1 );
::LeaveCriticalSection( &criticalSection1 );
::LeaveCriticalSection( &criticalSection1 );
criticalSection1;
return 0;
}
EnterCriticalSection의 같은 핸들을 2번 호출하는 경우 RecursionCount도 2의 값으로 변경됩니다. LeaveCriticalSection도 호출 횟수에 따라 RecursionCount를 동일하게 감소 시킵니다. 하지만 LockCount는 호출 횟수와 관계없이 1번만 변경 됩니다. OwingThread는 잠금 건 스레드의 따라 값이 변경 됩니다.
정리하면 CRITICAL_SECTION은 다양한 부가 기능을 통해서 API를 호출하는 사용자가 편하게 사용할 수 있도록 도와 줍니다. 이는 사용자에게 잘못 사용 해도 당장 문제가 되지 않게 하거나 혹은 다양한 정보를 제공할 수 있습니다. 하지만 데이터 규모가 커지고 관리하는 데이터가 늘어나서 성능에 문제를 가져올 수 있습니다. 특히 힙 할당 하는 코드가 있기 때문에 이는 성능에 문제가 될 수 있습니다.
CRITICAL_SECTION이 보조를 위해 데이터를 많이 사용하지만 반면에 SRWLock은 그런 기능을 지원하지 않습니다. CRITICAL_SECTION과는 달리 SRWLock은 Recursive Lock을 허용하지도 않고 해당 상황을 추적하는 관련 데이터도 없습니다. 이는 성능 속도가 빠르고 동적 메모리 할당이 필요하지 않도록 설계되었기 때문이라고 합니다. 데이터 구조를 살펴보면 구성이 매우 간단함을 확인할 수 있습니다.
typedef struct _RTL_SRWLOCK {
PVOID Ptr;
} RTL_SRWLOCK, *PRTL_SRWLOCK;
데이터 구조는 매우 간단해서 성능을 우선적으로 설계했다는 것이 이해가 됩니다. 실제로 어떻게 되는지 한번 배타적 락 획득을 여러번 해보고 공유 락 획득을 여러 번 해봐서 어떤 일이 발생하는지 직접 확인 해보겠습니다.
int main()
{
SRWLOCK srwLock;
AcquireSRWLockShared( &srwLock );
AcquireSRWLockShared( &srwLock );
AcquireSRWLockExclusive( &srwLock );
AcquireSRWLockExclusive( &srwLock );
return 0;
}
실험 결과는 AcquireSRWLockExclusive를 Release를 하지 않고 두번 이상 호출하면 무한 루프에 빠지게 됩니다. 하지만 AcquireSRWLockShared는 여러 번 호출해도 문제가 없습니다. 아마 WriterLock과 ReadLock의 차이점 때문에 그런 것으로 보입니다.
따라서 정리하면 CRITICAL_SECTION는 내부에 LockCount 및 스레드 정보 등 다양한 기능과 정보를 내포하고 있습니다. 그리고 Recursive Lock 획득해도 문제가 없습니다. 반면에 SRWLock은 그런 기능들을 지원하지 않습니다. 왜냐하면 성능 최적화 및 힙 할당을 최소화 하기 위해서 입니다. 그래서 Recursive Lock을 얻게 되면 무한 루프 등 예기치 못하는 문제가 발생합니다. 따라서 해당 스팩을 잘 알고 사용하면 개발하는 데 많은 도움이 될 것입니다.