크리티컬 섹션은 스레드 간 공유 자원에 대해서 배타적으로 접근해야 하는 작은 코드의 집합을 의미합니다.
공유자원를 "원자적"으로 수행 할 수 있도록 하는데
"원자적"이란 공유자원에 현재 스레드가 접근 중에는 다른 스레드가 접근할 수 없다는 것을 말합니다.
크리티컬 섹션은 하나의 프로세스 안에서 스레드 간의 동기화 객체를 제어합니다.
프로세스 간의 동기화 객체는 제어할 수 없습니다. 이럴 때는 커널 모드의 동기화 객체를 사용해야 합니다.
(윈도우의 mutex 커널오브젝트 같은)
크리티컬 섹션 사용방법
함수정보 | 간단한 사용 설명 |
InitializeCriticalSection | 크리티컬 섹션 객체를 초기화합니다. |
DeleteCriticalSection | 소유되지 않은 크리티컬 섹션의 리소스를 반환합니다. |
InitializeCriticalSectionAndSpinCount | 크리티컬 섹션 객체를 초기화하면서 SpinCount를 셋팅합니다. |
EnterCriticalSection | 크리티컬 섹션 객체의 소유권을 요청합니다. |
TryEnterCriticalSection | 크리티컬 섹션 객체의 소유권을 요청하고 성공하면 TRUE, 실패하면 FALSE |
SetCriticalSectionSpinCount | 크리티컬 섹션 객체의 SpinCount를 설정합니다. |
LeaveCriticalSection | 크리티컬 섹션의 객체의 소유권을 반납합니다. |
위의 함수들은 크리티컬 섹션 관련 API 함수이며 간단한 설명을 적어 놓았습니다.
아래에 해당 API에 대한 내부동작 방식을 적어 놓았습니다.
크리티컬 섹션을 사용하는 간단한 예를 보고 설명드리겠습니다.
using namespace std;
int g_nSum = 0;
CRITICAL_SECTION g_criticalsection;
unsigned __stdcall ThreadInterLock(void* pArg)
{
for (int i = 0; i < 100000; i++)
{
// 크리티컬 섹션 객체를 소유권을 요청합니다.
EnterCriticalSection(&g_criticalsection);
g_nSum += 1;
// 크리티컬 섹션 객체를 소유권을 반납합니다.
LeaveCriticalSection(&g_criticalsection);
}
return 1;
}
int _tmain(int argc, _TCHAR* argv[])
{
// 크리티컬 섹션 객체를 초기화하고 SpinCount를 1000으로 셋팅합니다.
BOOL bInit = InitializeCriticalSectionAndSpinCount(&g_criticalsection, 1000);
if (bInit)
{
//스레드 숫자만큼 ThreadInterLock 수행 스레드를 실행 시킵니다.
HANDLE hCSThreads[kTEST_THREAD_COUNT] = { 0, };
ElapsedTimeOutput elapsed("InterLocked Stack Thread Run ");
for (DWORD dwThreadIdx = 0; dwThreadIdx < kTEST_THREAD_COUNT; ++dwThreadIdx)
{
unsigned ThreadId = 0;
hCSThreads[dwThreadIdx] = (HANDLE)_beginthreadex(NULL, NULL, ThreadInterLock, nullptr, NULL, &ThreadId);
}
//모든 스레드 객체가 종료될 때까지 대기합니다.
DWORD dwWaitResult = WaitForMultipleObjects(kTEST_THREAD_COUNT, hCSThreads, TRUE, INFINITE);
if (WAIT_OBJECT_0 != dwWaitResult)
{
cout << "Fail WaitForMultipleObjects Result = " << dwWaitResult << endl;
}
// 크리티컬 섹션 객체의 리소스를 반환합니다.
DeleteCriticalSection(&g_criticalsection);
cout << "g_nSum = " << g_nSum << endl;
}
return 1;
}
크리티컬 섹션 객체를 기반으로 g_nSum 객체를 공유자원으로 보호하면서 값을 변경하는 로직입니다.
사용하기 편한 커스텀 크리티컬 섹션 클래스
namespace
{
const BYTE kLOCKTYPE_NONE = 0;
const BYTE kLOCKTYPE_EXCLUSIVE = 1;
const BYTE kLOCKTYPE_SHARED = 2;
}
/**
* @brief 크리티컬 섹션을 사용하기 쉽게 한 Wrapper 클래스
* @details 크리티컬 섹션의 초기화 및 해제 작업을 하지 않아도 됩니다. Lock 및 UnLock의 인터페이스를 통일합니다ㅣ.
* @author woong20123
* @date 2020-02-15
*/
class CSimpleCriticalSection
{
public:
CSimpleCriticalSection() : m_bInit(FALSE) { InitializeCriticalSection(&m_cs); m_bInit = TRUE; }
CSimpleCriticalSection(DWORD dwSpinCount) : m_bInit(FALSE) { m_bInit = InitializeCriticalSectionAndSpinCount(&m_cs, dwSpinCount); }
~CSimpleCriticalSection() { DeleteCriticalSection(&m_cs); }
void Lock(BYTE byLockType = kLOCKTYPE_EXCLUSIVE) { EnterCriticalSection(&m_cs); }
void Unlock(BYTE byLockType = kLOCKTYPE_EXCLUSIVE) { LeaveCriticalSection(&m_cs); }
BOOL IsInit() { return m_bInit; }
#if (_WIN32_WINNT >= 0x0400)
BOOL TryLock() { return TryEnterCriticalSection(&m_cs); }//_WIN32_WINNT=0x400
#endif
private:
CRITICAL_SECTION m_cs;
BOOL m_bInit;
};
/**
* @brief Scope범위에서만 Lock이 설정되도록 하는 클래습니다.
* @details 템플릿인자 클래스는 Lock 및 UnLock함수를 구현해야 합니다.
* @author woong20123
* @date 2020-02-15
*/
template<typename SimpleLockType>
class CScopeLock
{
public:
CScopeLock(SimpleLockType* pcs, BYTE byLockType = kLOCKTYPE_EXCLUSIVE) : m_pcsObjcect(pcs), m_byLockType(byLockType)
{
if (m_pcsObjcect && m_pcsObjcect->IsInit())
{
m_pcsObjcect->Lock(m_byLockType);
}
}
~CScopeLock()
{
if (m_pcsObjcect && m_pcsObjcect->IsInit())
{
m_pcsObjcect->Unlock(m_byLockType);
}
}
private:
// CFSScopeLock은 NonCopyable
CScopeLock(const SimpleLockType&);
const CScopeLock<SimpleLockType>& operator=(const SimpleLockType&);
SimpleLockType* m_pcsObjcect;
BYTE m_byLockType;
};
CSimpleCriticalSection 클래스는 크리티컬 섹션의 초기화 및 해제 작업을 자동으로 지원합니다.
인터페이스를 통일하여 나중에 통합적인 LOCK 클래스 관리를 할 수 있도록 합니다.
CScopeLock 클래스는 템플릿 인자로 전달된 객체를 Scope안에서만 Lock이 되도록 해줍니다.
// Scope 범위 안에서만 Critical Section의 Lock이 설정됩니다.
{
CScopeLock<CSimpleCriticalSection> ScopeLock(&g_cs);
g_StackData.push(pTestData);
}
크리티컬 섹션의 내부 동작 방식
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;
크리티컬 섹션의 내부 동작을 이해하기 위해서는 크리티컬 섹션의 구조체 정보를 알아야 합니다.
InitializeCriticalSection류의 함수 동작 방식
초기화 함수가 호출되기 전에 크리티컬 섹션의 객체는 아무런 수정이 없는 상태로 설정됩니다.
이 상태에서 InitializeCriticalSection이나 InitializeCriticalSectionAndSpinCount함수를 호출하게 되면
아래와 같이 초기화됩니다.
InitializeCriticalSectionAndSpinCount(&criticalsection, 1000);
초기화가 진행되어야 실질적인 크리티컬 사용이 가능한 상태가 됩니다.
LockCount는 -1이 되고 RecursionCount가 0으로 세팅됩니다.
EnterCriticalSection 동작 방식
EnterCriticalSection 함수가 호출되면 CRITICAL_SECTION 구조체의 OwingThread를 확인해서 공유 자원이
어떤 스레드에 의해서 소유되어 있는지 확인합니다.
- 크리티컬 섹션 객체를 소유한 스레드가 없을 때
- CRITICAL_SECTION 구조체를 갱신하여 해당 스레드가 공유자원을 획득했음을 설정한 후 바로 리턴됩니다.
- LockCount는 -2가 되고 RecursionCount는 1로 OwingThread는 해당 함수를 호출한 스레드로 세팅됩니다.
- 함수는 바로 리턴되며 이후 로직을 수행합니다.
- 이미 다른 스레드가 공유자원에 접근한 상태일 때
- 만약 SpinCount가 설정되어 있다면 SpinCount만큼 유저모드에서 해당 객체의 획득을 요청합니다.
- 스핀 락이 모두 실패한다면 LockSemaphore 객체를 사용해서 해당 스레드를 대기 상태(커널모드)로 만듭니다.
- 이후에 해당 크리티컬 섹션을 점유 중인 스레드에서 LeaveCriticalSection 함수를 호출해서 RecursionCount값이 0이 되면 대기 중인 스레드중 하나가 활성화됩니다.
- 크리티컬 섹션을 점유한 스레드에서 계속해서 EnterCriticalSection 함수를 호출하면 RecursionCount값이 1씩 증가합니다.
- 크리티컬 섹션을 점유한 스레드는 크리티컬 섹션의 점유를 해제하기 위해서는 RecursionCount 횟수만큼 LeaveCriticalSection을 호출해야 합니다.
크리티컬 섹션의 타임 아웃
크리티컬 섹션의 경우 EXCEPTION_POSSIBLE_DEADLOCK이 발생될 수 있습니다.
RegistEdit의 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\CriticalSectionTimeout
에 등록된 값보다 큰 시간 동안 점유가 된다면 발생합니다. 단위는 초이며 기본값은 2592000초(30일)입니다.
이 값을 수정할 수는 있지만 너무 적은 값으로 수정하면 문제가 발생할 수 있습니다.
LeaveCriticalSection 동작 방식
- 해당 함수가 호출되면 Recursion Count값이 감소합니다.
- RecursionCount 값이 0이 되면 OwiningThread값이 초기화되고 다른 스레드에 의해서 점유될 수 있습니다.
- 만약 대기중인 스레드가 있다면 해당 스레드중에 하나의 스레드에게 크리티컬 섹션의 점유가 이전됩니다.
DeleteCriticalSection 동작 방식
- 크리티컬 섹션 객체의 리소스를 초기화합니다.
크리티컬 섹션과 스핀 락
다른 스레드가 점유 중인 공유자원을 획득하기 위해서 EnterCriticalSection 함수를 호출하면 해당 스레드는
대기 상태로 전환되는데 이 작업은 유저 모드에서 커널 모드로 전환되는 작업이라서 비용 큰 작업입니다.
만약 다른 스레드가 짧은 시간 동안만 공유자원을 사용한다면 유저 모드에서 커널 모드로 전환되는 도중에
공유자원의 소유권을 반환할 수도 있기 때문에 비효율적일 수 있습니다.
이러한 상황을 개선하기 위해서 크리티컬 섹션에 스핀 락을 추가하였습니다.
주의할 사항으로 단일 프로세서 머신의 경우 SpinCount 값이 무시됩니다.
'c++ > windows' 카테고리의 다른 글
[Window c++] I/O completion port ( IOCP) (1) | 2020.02.20 |
---|---|
[Window c++] 슬림 리더-라이터 락 (SRWLock) (2) | 2020.02.16 |
[windows] 멀티 프로세서 환경에서의 CPU 캐시라인 (1) | 2020.02.15 |
[window c++] InterLocked 함수들 (2) | 2020.02.12 |
[window c++] 스레드 스케줄링 (0) | 2020.02.09 |