크리티컬 섹션은 스레드 간 공유 자원에 대해서 배타적으로 접근해야 하는 작은 코드의 집합을 의미합니다.

 공유자원 "원자적"으로 수행 할 수 있도록 하는데

 "원자적"이란 공유자원에 현재 스레드가 접근 중에는 다른 스레드가 접근할 수 없다는 것을 말합니다. 

 크리티컬 섹션은 하나의 프로세스 안에서 스레드 간의 동기화 객체를 제어합니다. 

 프로세스 간의 동기화 객체는 제어할 수 없습니다. 이럴 때는 커널 모드의 동기화 객체를 사용해야 합니다.

 (윈도우의 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);

InitializeCriticalSectionAndSpinCount 함수 호출후 크리티컬 섹션 객체 상태

 초기화가 진행되어야 실질적인 크리티컬 사용이 가능한 상태가 됩니다.

 LockCount는 -1이 되고 RecursionCount가 0으로 세팅됩니다.

EnterCriticalSection 동작 방식

 EnterCriticalSection 함수가 호출되면 CRITICAL_SECTION 구조체의 OwingThread를 확인해서 공유 자원이

 어떤 스레드에 의해서 소유되어 있는지 확인합니다. 

  • 크리티컬 섹션 객체를 소유한 스레드가 없을 때
    • CRITICAL_SECTION 구조체를 갱신하여 해당 스레드가 공유자원을 획득했음을 설정한 후 바로 리턴됩니다. 
    • LockCount는 -2가 되고 RecursionCount는 1로 OwingThread는 해당 함수를 호출한 스레드로 세팅됩니다. 
    • 함수는 바로 리턴되며 이후 로직을 수행합니다.

EnterCrticalSection함수 호출 후 크리티컬 섹션 객체 상태

  • 이미 다른 스레드가 공유자원에 접근한 상태일 때 
    • 만약 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값이 초기화되고 다른 스레드에 의해서 점유될 수 있습니다.
  • 만약 대기중인 스레드가 있다면 해당 스레드중에 하나의 스레드에게 크리티컬 섹션의 점유가 이전됩니다.  

LeaveCriticalSection 함수 호출시 크리티컬 섹션 객체 상태

 

DeleteCriticalSection 동작 방식

  •  크리티컬 섹션 객체의 리소스를 초기화합니다. 

크리티컬 섹션과 스핀 락

 다른 스레드가 점유 중인 공유자원을 획득하기 위해서 EnterCriticalSection 함수를 호출하면 해당 스레드는

 대기 상태로 전환되는데 이 작업은 유저 모드에서 커널 모드로 전환되는 작업이라서 비용 큰 작업입니다. 

 만약 다른 스레드가 짧은 시간 동안만 공유자원을 사용한다면 유저 모드에서 커널 모드로 전환되는 도중에 

 공유자원의 소유권을 반환할 수도 있기 때문에 비효율적일 수 있습니다. 

 이러한 상황을 개선하기 위해서 크리티컬 섹션에 스핀 락을 추가하였습니다.

 주의할 사항으로 단일 프로세서 머신의 경우 SpinCount 값이 무시됩니다.

+ Recent posts