SRWLock(Slim Reader-Writer Lock)은 크리티컬 섹션과 마찬가지로 공유자원을 다수의 스레드가 접근할 때

 공유자원을 보호할 목적으로 사용합니다.

 크리티컬 섹션과의 차이점은 공유 자원을 읽기만 하는 스레드와 값을 수정하려고 하는 스레드를 구분해서 

 관리한다는 점 입니다. 공유자원을 읽기만 하는 Reader 스레드들은 동시에 수행되어도 값을 수정시키지 않기 때문에

 동시에 접근해도 무방하며

 공유자원을 수정하려는 Writer 스레드가 공유자원에 접근하는 경우에만 배타적 접근이 필요합니다. 

SRWLock 사용 방법

함수정보 간단한 사용 설명
InitializeSRWLock SRWLock 객체를 초기화합니다. 
AcquireSRWLockShared SRWLock 객체에 대한 읽기 권한을 요청 합니다.
ReleaseSRWLockShared SRWLock 객체에 대한 읽기 권한을 해제 합니다.
AcquireSRWLockExclusive SRWLock 객체에 대한 읽기/쓰기 권한을 요청 합니다.
ReleaseSRWLockExclusive SRWLock 객체에 대한 읽기/쓰기 권한을 요청 합니다.
TryAcquireSRWLockExclusive SRWLock 객체에 대한 읽기/쓰기 권한을 요청합니다. 성공하면 권한을 획득하고 실패하면 바로 리턴됩니다. 성공시 NonZero, 실패시 Zere를 반환합니다.
TryAcquireSRWLockShared SRWLock 객체에 대한 읽기 권한을 요청합니다. 성공하면 권한을 획득하고 실패하면 바로 리턴됩니다. 성공시 NonZero, 실패시 Zere를 반환합니다.

아래 예제를 통해서 사용방법을 알아봅시다. 

using namespace std;

int g_nSum = 0;
int g_tot
SRWLOCK g_srwlock;


// g_nSum 공유자원을 수정합니다.
void ThreadWriterLock()
{
	AcquireSRWLockExclusive(&g_srwlock);
	g_nSum += 1;
	ReleaseSRWLockExclusive(&g_srwlock);
}

// g_nSum 공유자원을 읽습니다.
void ThreadReaderLock()
{
	AcquireSRWLockShared(&g_srwlock);
	int current_sum = g_nSum;
	ReleaseSRWLockShared(&g_srwlock);
}

int main()
{	
	InitializeSRWLock(&g_srwlock);

	vector<thread> threadContainer;
	for (int i = 0; i < 100; i++)
	{
		threadContainer.emplace_back(ThreadWriterLock);
		threadContainer.emplace_back(ThreadReaderLock);
	}

	for (auto & t : threadContainer)
	{
		if (t.joinable())
			t.join();
	}
	
	return 0;
}

g_nSum 데이터를 SRWLock 객체를 사용해서 공유자원으로 보호하면서 수정하는 예입니다. 

ThreadWriterLock 함수는 읽기/쓰기 전용 함수인 SRWLockExclusive 함수를 사용합니다. 

ThreadReaderLock함수는 쓰기 전용 함수인 SRWLockShared 함수를 사용합니다. 

사용하기 편한 커스텀 크리티컬 섹션 클래스 

namespace
{
    const BYTE kLOCKTYPE_NONE = 0;
    const BYTE kLOCKTYPE_EXCLUSIVE = 1;
    const BYTE kLOCKTYPE_SHARED = 2;
}

/**
 * @brief SRWLock을 사용하기 쉽게 한 Wrapper 클래스
 * @details 인터페이스를 통일합니다. 초기화 작업을 단순하게 합니다. 
 * @author woong20123
 * @date 2020-02-16
 */
class CSimpleSRWLock
{
public:
    CSimpleSRWLock() : m_bInit(FALSE) { InitializeSRWLock(&m_SRWLock); m_bInit = TRUE; }
    ~CSimpleSRWLock() { }

    void Lock(BYTE byLockType) {
        switch(byLockType)
        {
        case kLOCKTYPE_EXCLUSIVE:
            AcquireSRWLockExclusive(&m_SRWLock);
            break;
        case kLOCKTYPE_SHARED:
            AcquireSRWLockShared(&m_SRWLock);
            break;
        }
    }
    void Unlock(BYTE byLockType ) { 
        switch (byLockType)
        {
        case kLOCKTYPE_EXCLUSIVE:
            ReleaseSRWLockExclusive(&m_SRWLock);
            break;
        case kLOCKTYPE_SHARED:
            ReleaseSRWLockShared(&m_SRWLock);
            break;
        }
    }
    BOOL IsInit() { return m_bInit; }
    BOOL TryLock(BYTE byLockType) { 
        boolean bRet = 0;
        switch (byLockType)
        {
        case kLOCKTYPE_EXCLUSIVE:
            bRet = TryAcquireSRWLockExclusive(&m_SRWLock);
            break;
        case kLOCKTYPE_SHARED:
            bRet = TryAcquireSRWLockShared(&m_SRWLock);
            break;
        }
        return bRet != 0 ? TRUE : FALSE;
    }
private:
    SRWLOCK             m_SRWLock;
    BOOL                m_bInit;
};


/**
 * @brief Scope범위에서만 Lock이 설정되도록 하는 클래습니다.
 * @details 템플릿인자 클래스는 Lock 및 UnLock함수를 구현해야 합니다. 
 * @author woong20123
 * @date 2020-02-15
 */
template<typename SimpleLockType>
class CScopeLock
{
public:
    CScopeLock(const 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:
    // CScopeLock은 NonCopyable
    CScopeLock(const SimpleLockType&);
    const CScopeLock<SimpleLockType>& operator=(const SimpleLockType&);

    SimpleLockType* m_pcsObjcect;
    BYTE            m_byLockType;
};

 CSimpleSRWLock클래스는 크리티컬 섹션의 초기화 작업을 자동으로 지원합니다.

 인터페이스를 통일하여 나중에 통합적인 LOCK 클래스 관리를 할 수 있도록 합니다.

 CScopeLock 클래스는 템플릿 인자로 전달된 객체를 Scope안에서만 Lock이 되도록 해줍니다. 

CSimpleSRWLock g_simpleSRWLock;

// g_nSum 공유자원을 수정합니다.
void ThreadWriterLock()
{
    for (int i = 0; i < 10000; i++)
    {
        CScopeLock<CSimpleSRWLock> ScopeSRWLock(&g_simpleSRWLock, kLOCKTYPE_EXCLUSIVE);
        g_nSum += 1;
    }
}

// g_nSum 공유자원을 읽습니다.
void ThreadReaderLock()
{
    for (int i = 0; i < 10000; i++)
    {
        CScopeLock<CSimpleSRWLock> ScopeSRWLock(&g_simpleSRWLock, kLOCKTYPE_SHARED);
        int current_sum = g_nSum;
    }
}

 

SRWLock의 내부 동작 방식

typedef struct _RTL_SRWLOCK {                            
        PVOID Ptr;                                       
} RTL_SRWLOCK, *PRTL_SRWLOCK;       

 RTL_SRWLOCK의 내부의 구조체 정보입니다. 아주 단순한 형태입니다.

 VOID형 포인터 데이터만 들고 있습니다. 64비트 메모리 하나만으로 객체의 소유권을 설정할 수 있어서 

 다른 Lock객체들에 비해서 빠른 성능 보여 줍니다. 

IntializeSRWLock 함수 동작 방식

 해당 함수를 호출하면 단순히 SRWLOCK 객체를 초기화 시킵니다.

IntializeSRWLock 메모리 초기화

 실제로 해당 함수를 수행해보면 Ptr을 0으로 초기화시킵니다.

SRWLOCK srwlock;
InitializeSRWLock(&srwlock);
// RTL_SRWLOCK_INIT = {0} 같습니다.
SRWLOCK srwlock2 = RTL_SRWLOCK_INIT;

IntializeSRWLock함수는 구조체를 0으로 세팅하는 RTL_SRWLOCK_INIT와 동일한 작업을 합니다.

AcquireSRWLockExclusive 함수 동작 방식

 점유되지 않은 SRWLock 객체에 AcquireSRWLockExclusive 함수를 호출하면 SRWLock객체의 ptr 값이 변경됩니다.

AcquireSRWLockExclusive 호출시

 ptr의 16진수 1번째 비트가 켜집니다. 

 AcquireSRWLockExclusive로 점유된 상태에서는 AcquireSRWLockExclusive 또는 AcquireSRWLockShared 함수를 

 호출하면 해당 스레드는 대기 상태로 전환됩니다. 

 크리티컬 섹션과는 다르게 SRWLock은 반복적인 락 획득이 불가능하기 때문에 객체를 소유한 스레드에서 다시

 AcquireSRWLockExclusive 호출하면 데드락 상태에 빠집니다. (AcquireSRWLockShared 호출해도 동일합니다.) .

 주의해서 사용해야 합니다. 

AcquireSRWLockShared 함수 동작 방식

 점유되지 않은 SRWLock 객체에 AcquireSRWLockShared 함수를 호출하면 SRWLock객체의 ptr 값이 변경됩니다.

AcquireSRWLockShared  호출시

 ptr의 16진수 1번째, 2번째 비트가 켜집니다.내부 동작은 정확히 알 수 없지만 비트 값을 체크해서 Shared인지

 Exclusive인지 구분하는 것 같습니다. 

 AcquireSRWLockShared로 점유된 상태에서는 AcquireSRWLockShared를 중복해서 사용할 수 있습니다. 

 AcquireSRWLockExclusive함수를 호출하면 대기 상태로 전환됩니다. 

 AcquireSRWLockShared를 중복해서 호출하게 되면 ptr의 16진수 2번째 비트가 1씩 증가합니다.

AcquireSRWLockShared  6회 중복 호출시

 위의 사진은 AcquireSRWLockShared함수를 6번 호출했을 경우의 ptr 메모리 값입니다. 2번째 비트가 6이 되었습니다.

ReleaseSRWLockExclusive 함수 동작 방식

 ReleaseSRWLockExclusive 함수를 호출하면 SRWLock객체의 ptr의 16진수 1번째 비트가 0으로 세팅합니다. 

    // 읽기/쓰기 권한 요청
    AcquireSRWLockExclusive(&srwlock);  // ptr값 = 0x0000000000000001
    // 읽기/쓰기 권한 해제
    ReleaseSRWLockExclusive(&srwlock);  // ptr값 = 0x0000000000000000

ReleaseSRWLockExclusive 호출시

이렇게 정상적으로 Exclusive 함수를 짝으로 사용하면 비트가 제거되면서 아무 스레드도 소유하지 않은 상태가 됩니다. 

 만일 Shared로 Lock을 획득하고 Exclusive로 락을 해제하면 어떻게 될까요?

    // 읽기 권한 요청
    AcquireSRWLockShared(&srwlock);	// ptr값 = 0x0000000000000011
    // 읽기/쓰기 권한 해제시?!
    ReleaseSRWLockExclusive(&srwlock);	// ptr값 = 0x0000000000000010

16진수의 1번째 비트만 0으로 만들고 2번째 비트는 설정하지 않습니다. 예상치 못하게 메모리가 오염된 상태로

적용되고 AcquireSRWLockExclusive나 AcquireSRWLockShared가 호출돼도 문제를 인식 못하고 수행이 가능합니다. 

    // 읽기 권한 요청
    AcquireSRWLockShared(&srwlock);	// ptr값 = 0x0000000000000011
    // 읽기/쓰기 권한 해제시?!
    ReleaseSRWLockExclusive(&srwlock);	// ptr값 = 0x0000000000000010
    // 읽기/쓰기 권한 요청
    AcquireSRWLockExclusive(&srwlock);	// ptr값 = 0x0000000000000011

 위의 순서대로 호출되면 최종적으로 읽기/쓰기 권한을 요청했는데 읽기 권한으로 요청된 것처럼 ptr이 세팅되고

 문제없이 코드가 수행됩니다.

ReleaseSRWLockShared 함수 동작 방식

 해당 함수를 호출하면 SRWLock객체의 ptr의 16진수 2번째 비트 값을 1씩 감소 시킵니다.

 만일 ptr의 16진수 2번째 비트 값이 0이 되면 ptr의 16진수 1번째 비트 값을 0으로 셋팅합니다.

    AcquireSRWLockShared(&srwlock); // ptr값 = 0x0000000000000011
    ReleaseSRWLockShared(&srwlock); // ptr값 = 0x0000000000000000
    AcquireSRWLockShared(&srwlock); // ptr값 = 0x0000000000000011
    AcquireSRWLockShared(&srwlock); // ptr값 = 0x0000000000000021
    ReleaseSRWLockShared(&srwlock); // ptr값 = 0x0000000000000011

 위의 예제와 반대로  만일 Exclusive로 Lock을 획득하고 Shared로 락을 해제 하면 어떻게 될까요?

    AcquireSRWLockExclusive(&srwlock);	// ptr값 = 0x0000000000000001
    ReleaseSRWLockShared(&srwlock);	// ptr값 = 0xfffffffffffffff1

 완전히 메모리가 이상한 상태로 변환됩니다. 이후에는 Exception이 발생하지 않고 비정상적인 로직이 수행됩니다. 

SRWLock을 사용시 주의 할점

 SRWLock의 경우 빠른 속도를 위해서 포인터 하나로 공유 객체를 제어합니다. 

 빠르다는 이점이 있지만 안전성 체크에는 취약합니다.

 사용자의 잘못된 호출이나 스레드 소유권을 검사하지 않기 때문에 함수 호출 순서를 주의해서 사용하도록 해야합니다. 

크리티컬 섹션과 SRWLock 성능 비교 

https://jungwoong.tistory.com/5

 

[windows] 유저모드 스레드 동기화 방법별로 속도 측정

윈도우에서 스레드 간의 동기화를 위한 다양한 방법을 사용해서 속도를 측정해보도록 합니다. 테스트 환경 : visual studio 2017, cpu i7-4790(코어4개, 로직스레드 8개) ElapsedTimeOutput클래스 객체는 https://..

jungwoong.tistory.com

 위 글에서 다양한 동기화 객체를 비교해 보았는데 코어 4 하이퍼 스레드 8인 cpu, visual studio 2017인 환경에서

 크리티컬 섹션에 비해서 SRWLock의 성능이 좋은 걸로 확인되었습니다.

 동기화가 필요한데 중복으로 락을 획득할 필요가 없다면 SRWLock을 사용하는 것을 권장드립니다. 

 (대부분의 상황에서는 같은 스레드에서 중복적으로 락을 획득하는 로직은 필요가 없습니다.)

+ Recent posts