공부했던 걸 기록하는 차원에서 Windows에서 제공하는 유저 동기화 객체인 CRITICAL_SECTION과 SRW_LOCK의 동작 방식에 대해 간단히 알아봄.
#include <iostream>
#include <Windows.h>
#include <thread>
CRITICAL_SECTION g_cs;
void Test()
{
EnterCriticalSection(&g_cs);
Sleep(2000);
LeaveCriticalSection(&g_cs);
}
int main()
{
InitializeCriticalSection(&g_cs);
EnterCriticalSection(&g_cs);
std::thread t1 = std::thread(Test);
Sleep(10000);
LeaveCriticalSection(&g_cs);
if (t1.joinable())
t1.join();
DeleteCriticalSection(&g_cs);
}
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;


LockCount와 SpinCount를 변경한 뒤 DebugInfo값까지 0xffffffff로 초기화한다. (Debug값 변경은 스크린 샷에 없음)

lock btr 명령어를 통해 0번 비트를 0으로 변경한 후 이전 값을 test -> 위의 그림에서 jae분기를 탄다. (주황색 부분)
분기를 타서 현재 CS에 현재 진입한 스레드가 CS를 소유한 스레드와 동일한지 확인하는 작업을 한다.
2-1 (동일할 때) -> CS의 RecursionCount만 1로 증가시키고 종료한다.
2-2 (동일하지 않을 때) -> 다른 스레드가 진입을 시도하려고 하는 것이기 때문에 다른 로직으로 분기를 탄다.

RtlpEnterCriticalSectionContended를 함수를 호출..
정확히는 모르겠지만 저장된 SpinCount를 이용해 상단부 / 하단부를 나눠서 eax / ebx에 저장하고 작업을 한다.



4-2 Spin횟수를 다 소모했음에도 lock을 획득할 수 없는 상태.
RtlWaitOnCriticalSection 함수 내부에서
RtlpWaitOnAddressWithTimeOut 함수 -> _NTWaitForAlertByThreadId (시스템콜) 호출.

CS의 RecusionCount를 1감소시키고 0이 아니라면 리턴한다.
CS의 스레드 ID를 0으로 초기화하고, atomic하게 LockCount를 0xffffffff로 변경한 후 리턴.

Windows 8 이후의 Windows에서는 세마포어가 아닌 WaitForAddress 방식을 사용한다.라고 알고 있다.
https://learn.microsoft.com/ko-kr/windows/win32/api/synchapi/nf-synchapi-waitonaddress