스레드 간 동기화를 위해서는 락 메커니즘을 활용합니다. 락을 획득한 상태에서 예외가 발생하면 락을 반환해야 하는데
예외 케이스가 다양할 경우 각 예외 구문마다 락 반환코드를 넣는 것은 번거롭습니다.
이번 포스팅에서는 지역변수의 스코프 내 유효 특성을 이용하여 함수 종료 시 획득한 락도 자동으로 반환되는 일명 Autolock class에 대해 예제기반으로 설명합니다.
1. 커널모드, 유저모드
락은 크게 커널모드 동작방식, 유저모드 동작방식으로 구분되며 커널모드의 대표적인 락은 뮤텍스이며 중첩락을 허용하는 특징이 있습니다. 유저모드의 대표적인 락은 크리티컬섹션입니다. 현업에서 뮤텍스, 크리티컬섹션의 사용 사례를 다양하게 접했지만 뮤텍스의 중첩을 활용하는 경우는 거의 보지 못하였습니다. 또한 Lock/Unlock이 빈번할 경우 커널모드 동작방식의 락이 퍼포먼스 측면에서 많이 불리하므로 커널모드 방식은 필요할 때만 사용하는 것이 적절합니다.
2. CriticalSection과 Slim Reader/Writer (SRW) Locks
공유메모리 역할의 SharedArray가 있고 데이터를 기록하는 스레드를 WriterA, 데이터를 읽어가는 스레드가 ReaderB, ReaderC, ReaderD가 있다고 가정합니다. CriticalSection을 사용하면 ReaderB, ReaderC, ReaderD 간에도 경쟁이 발생하여 읽기 작업에서도 Serialized Section이 발생합니다. 이를 개선할 수 있는 방식이 Slim Reader/Writer Lock(이하 SRW 락)입니다. SRW 락은 유저모드로 동작하므로 스레드 간 공유자원에 사용할 수 있고 프로세스 간 공유자원에는 사용할 수 없습니다. 대신 속도가 빠르고 메모리 점유가 적은 장점이 있습니다.
SRW 락은 두 가지 모드를 제공합니다.
- Shared mode : reader thread들이 shared mode로 접근하면 모든 스레드가 동시에 접근할 수 있어 citical section보다 퍼포먼스가 향상됩니다.
- Exclusive mode : 이 모드에서는 읽기/쓰기를 동시에 수행합니다. Shared mode보다 우선이기 때문에 오직 한 스레드만 진입 가능합니다.
3. SRW Lock 함수
- AcquireSRWLockExclusive, ReleaseSRWLockExclusive
- AcquireSRWLockShared, ReleaseSRWLockShared
- InitializeSRWLock
- SleepConditionVariableSRW
- TryAcquireSRWLockExclusive
- TryAcquireSRWLockShared
4. SRW Lock 예제 코드
SRW Lock 사용을 위해서는 SRWLOCK 변수가 필요합니다. 공유 구조체의 경우 맨 보통 맨 위에 SRWLOCK 타입 변수를 선언하여 사용합니다. 구조체 생성자에서 InitializeSRWLock으로 SRWLOCK 변수를 초기화 합니다.
typedef struct _DS_CONF_SERVER {
SRWLOCK srwlock;
WORD wPortNo; // service port number
WORD wProtocolVersionMajor;
WORD wProtocolVersionMinor;
WORD wProtocolVersionPatch;
_DS_CONF_SERVER() {
ZeroMemory(this, sizeof(struct _DS_CONF_SERVER));
InitializeSRWLock(&srwlock);
}
} TST_DS_CONF_SERVER, *PTST_DS_CONF_SERVER;
SharedLock 사용 코드입니다.
LPVOID CDiagnostics::fpLookupDiagHandlerbyTrCode(LPCTSTR lpstrTrCode)
{
LPVOID lp;
BOOL boRet;
AcquireSRWLockShared(&(_m_stTrHandlerMap.srwlock));
boRet = _m_stTrHandlerMap.map.Lookup(lpstrTrCode, lp);
ReleaseSRWLockShared(&(_m_stTrHandlerMap.srwlock));
if (FALSE == boRet)
lp = nullptr;
return lp;
}
5. AutoScopeLock 필요성
함수 진입 후 특정 자료구조의 데이터가 정상인지 하나하나 체크한다고 가정할 때 각 if문에서 예외가 발생하면 예외문마다 ReleaseLock 함수를 호출해야 하는데 코딩 실수가 발생하여 버그를 생성할 수 있고 타이핑 노력이 많이 들어갑니다.
지역변수의 스코프 특성을 활용하여 아래와 같이 CAutoScopeSRWLOCKExclusive, CAutoScopeSRWLockShared를 만들어 쓰면 편리합니다. 함수 내 return 포인트가 여러 곳에 존재하지만 Lock 사용 코드는 함수 진입점에 한 번만 기술되어 있습니다. lock 변수가 지역변수로 생성될 때 생성자가 락을 획득하고, 지역변수가 해제될 때 소멸자가 호출되면서 소멸자에서 락을 릴리스합니다.
PTST_DSM_ITEM CDmssItem::pAlloc_PTST_DSM_ITEM_CHAIN(const WORD wQuantity)
{
...생략...
CAutoScopeSRWLOCKExclusive lock(&(pPool->srwlock));
if (pPool->wFreeItemsCnt < wQuantity) { /* Dynamic allocation */
DPRINTF("[WARN] pAlloc_PTST_DSM_ITEM_CHAIN wQuantity(%d), pPool->wFreeItemsCnt(%d)\n", wQuantity, pPool->wFreeItemsCnt);
pHead = new TST_DSM_ITEM;
pTail = pHead;
if (nullptr == pTail) {
return nullptr;
}
wAssigned = 1;
while (wAssigned < wQuantity) {
pTail->pNext = new TST_DSM_ITEM;
if (nullptr == pTail->pNext) {
break;
}
pTail = pTail->pNext;
wAssigned++;
};
if (wQuantity == wAssigned) {
return pHead; /* return dynamic allocated memory */
}
else { /* Cancel dynamic allocation */
while (pHead) {
pTail = pHead->pNext;
delete pHead;
pHead = pTail;
}
}
return nullptr;
}
...생략...
return pHead;
}
6. CAutoScope class 코드
아래 헤더파일을 프로그램에 추가 후 사용하면 됩니다.
6.1 CAutolock.h
#pragma once
namespace
{
const BYTE LOCKTYPE_NONE = 0;
const BYTE LOCKTYPE_EXCLUSIVE = 1;
const BYTE LOCKTYPE_SHARED = 2;
}
class CAutoScopeSRWLOCKShared
{
private:
PSRWLOCK m_psrwlock;
protected:
CAutoScopeSRWLOCKShared() { m_psrwlock = NULL; }
public:
CAutoScopeSRWLOCKShared(PSRWLOCK psrwlock) { AcquireSRWLockShared(psrwlock); m_psrwlock = psrwlock; }
~CAutoScopeSRWLOCKShared() { ReleaseSRWLockShared(m_psrwlock); m_psrwlock = NULL; }
static void Init(PSRWLOCK psrwlock) { InitializeSRWLock(psrwlock); }
};
class CAutoScopeSRWLOCKExclusive
{
private:
PSRWLOCK m_psrwlock;
protected:
CAutoScopeSRWLOCKExclusive() { m_psrwlock = NULL; }
public:
CAutoScopeSRWLOCKExclusive(PSRWLOCK psrwlock) { AcquireSRWLockExclusive(psrwlock); m_psrwlock = psrwlock; }
~CAutoScopeSRWLOCKExclusive() { ReleaseSRWLockExclusive(m_psrwlock); m_psrwlock = NULL; }
static void Init(PSRWLOCK psrwlock) { InitializeSRWLock(psrwlock); }
};
이상으로 지역변수의 스코프 특성을 활용하여 클래스 소멸자에서 자동으로 락을 해제할 수 있는 방법을 알아보았습니다.
'프로그래밍 > C | C++' 카테고리의 다른 글
IOCP 기반 다중 접속 및 데이터 처리-2 (0) | 2023.12.14 |
---|---|
IOCP 기반 다중 접속 및 데이터 처리-1 (0) | 2023.12.13 |
[MFC] MFC 스레드 예제 (0) | 2023.12.10 |
Windows환경 INI 설정 파일 활용 (0) | 2023.10.11 |
Multiplexing Client Connections-1 (0) | 2023.06.30 |