기본 이론
- 임계구간 코드가 여러 스레드에서 동시에 실행되는 일을 막는것
- 연산 시점(혹은 종료) 감지
- 동기화 객체를 이용해 구현
- Critical Section , Mutext, Semaphore, Event(Set/Reset 상태)
- Kernel Object라고 부른다.
스레드 실행 시점 동기화
T2 스레드에서 어떤 특정 시점 A,B에서 Set Event를 해주면 T1 스레드에서 해당 Event 객체를 WaitForSingleObject 에 넣어주면 Set 되는 순간을 알 수 있게 된다.
UINT WINAPI ThreadFunction(LPVOID pParam)
{
HANDLE hEvent = pParam;
std::cout << "ThreadFunction() - Begin" << std::endl;
_getch();
SetEvent(hEvent);
std::cout << "ThreadFunction() - SetEvent()" << std::endl;
std::cout << "ThreadFunction() - End" << std::endl;
return 0;
}
int main()
{
std::cout << "main() - Begin" << std::endl;
HANDLE hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
UINT nThreadId = 0;
HANDLE hThread = (HANDLE)::_beginthreadex(
NULL,
0,
ThreadFunction,
(LPVOID)hEvent,
0,
&nThreadId);
if (hThread == NULL) {
std::cout << "ERROR: Failed to begin thread." << std::endl;
return 0;
}
if (::WaitForSingleObject(
hEvent, INFINITE) == WAIT_OBJECT_0)
{
std::cout << "main() - WaitForSingleObject() return" << std::endl;
}
::WaitForSingleObject(hThread, INFINITE);
std::cout << "main() - Thread End" << std::endl;
::CloseHandle(hThread);
std::cout << "main() - End" << std::endl;
return 0;
}
worker thread에서 사용자의 input을 감지하고 event를 set하여서 main thread에서 그 event를 감지하고 종료하게 된다.
CreateEvent #bManualReset가 False인 경우 event가 get 되는 순간 reset이 된다.
만약 여러 스레드의 WaitForSingleObject가 get하려한다면 문제가 될 수 있다. 첫 번째 스레드만 해당 이벤트를 인지 할 수 있기 때문이다.
이때는 True로 해서 명시적으로 Reset하게 해줌으로서 모든 스레드가 해당 이벤트를 get할 수 있도록 할 수 있다.
임계영역 기반 동기화
#include <iostream>
#include <stdio.h>
#include <windows.h>
#include <process.h>
char* g_pszBuffer = NULL;
CRITICAL_SECTION g_cs;
void SetString(const char* pszData)
{
//::EnterCriticalSection(&g_cs);
if (g_pszBuffer != NULL)
{
free(g_pszBuffer);
g_pszBuffer = (char*)malloc(64);
sprintf_s(g_pszBuffer, 64, "%s", pszData);
}
else
{
g_pszBuffer = (char*)malloc(64);
sprintf_s(g_pszBuffer, 64, "%s", pszData);
}
//::LeaveCriticalSection(&g_cs);
}
BOOL GetString(char* pszData)
{
//::EnterCriticalSection(&g_cs);
if (pszData != NULL)
{
sprintf_s(pszData, 64, "%s", g_pszBuffer);
free(g_pszBuffer);
g_pszBuffer = NULL;
::LeaveCriticalSection(&g_cs);
return TRUE;
}
//::LeaveCriticalSection(&g_cs);
return FALSE;
}
UINT ThreadFunc1(LPVOID pParam)
{
while (TRUE)
{
::Sleep(1);
SetString("ThreadFunc1");
}
return 0;
}
UINT ThreadFunc2(LPVOID pParam)
{
while (TRUE)
{
::Sleep(1);
SetString("ThreadFunc2");
}
return 0;
}
int main()
{
//::InitializeCriticalSection(&g_cs);
UINT nThreadId = 0;
HANDLE hThread = (HANDLE)::_beginthreadex(
NULL,
0,
ThreadFunc1,
NULL,
0,
&nThreadId);
::CloseHandle(hThread);
hThread = (HANDLE)::_beginthreadex(
NULL,
0,
ThreadFunc2,
NULL,
0,
&nThreadId);
char szBuffer[64];
for (int i = 0; i < 5; ++i)
{
::Sleep(500);
GetString(szBuffer);
puts(szBuffer);
}
::WaitForSingleObject(hThread, INFINITE);
::CloseHandle(hThread);
//::DeleteCriticalSection(&g_cs);
return 0;
}
동기화 로직을 주석처리 해놓으면
g_pszBuffer = (char*)malloc(64);
디버그 모드로 실행시켜면 여기에 에러가 났다고 한다. 실제로 여기가 아니라
free(g_pszBuffer);
바로 위 로직인 여기서 에러가 난것이다. 이미 getString에서 이미 free되었는데 또 free하려니 문제가 된것이다. 디버그 모드에서 실제로 해당 g_pszBuffer의 주소를 찍어보면 dd 로 도배가되어있다. (free 되었다는 의미)
CRITICAL_SECTION g_cs;
위 변수 기반으로 동기화를 할 수 있다.
::InitializeCriticalSection(&g_cs);
::EnterCriticalSection(&g_cs);
thread safe logic
::LeaveCriticalSection(&g_cs);
::DeleteCriticalSection(&g_cs);
모든 스레드들이 다 차례로 thread safe logic안으로 들어오게 동기화 처리가 된다.
세마포어
세마포어를 이용한 동시성 제어
- 특정 영역 (실행 코드 구간)에 대해 2개 이상 n개 이하 스레드가 동시 접근 할 수 있도록 제어
- 서버 개발시 안정적인 서비스가 이루어지도록 동시 처리 사용자 세션 개수를 제한 하는것이 가장 대표적
thread safe logic을 1개의 스레드만 허용해주고 싶을 때는 CriticalSection을 쓰지만 2개이상일 때는 세마포어를 사용하게 된다. db의 connection pool이 그런 로직에 해당이 된다.
#include <iostream>
#include <stdio.h>
#include <process.h>
#include <Windows.h>
HANDLE g_hSema;
TCHAR g_StringList[10][64] = { 0 };
UINT WINAPI ThreadSemaphore(LPVOID pParam)
{
int nIndex = (int)pParam;
while (TRUE)
{
///////////////////////////////////////////////////////
::wsprintf(g_StringList[nIndex],
TEXT("%d thread is waiting!"), nIndex);
::Sleep(500);
///////////////////////////////////////////////////////
DWORD dwResult = ::WaitForSingleObject(g_hSema, INFINITE);
::wsprintf(g_StringList[nIndex],
TEXT("##%d thread is selected!##"), nIndex);
::Sleep(500);
::ReleaseSemaphore(g_hSema, 1, NULL);
}
return 0;
}
int main()
{
g_hSema = ::CreateSemaphore(NULL, 3, 3, NULL);
UINT nThreadId = 0;
HANDLE hThread = NULL;
for (int i = 0; i < 10; ++i)
{
hThread = (HANDLE)::_beginthreadex(
NULL,
0,
ThreadSemaphore,
(LPVOID)i,
0,
&nThreadId);
::CloseHandle(hThread);
}
while (1)
{
system("cls");
for (int i = 0; i < 10; ++i)
_putws(g_StringList[i]);
::Sleep(1000);
}
::CloseHandle(g_hSema);
}
g_hSema = ::CreateSemaphore(NULL, 3, 3, NULL); 를 통해 semaphore를 초기화해주고
DWORD dwResult = ::WaitForSingleObject(g_hSema, INFINITE); 에서 해당 kernel object를 대상으로 대기를 하게 된다. 이후로직은 3개의 스레드만 허용이 된다.
전혀 다른 프로세스에서의 접근도 막을 수 있다.
친화력(Affinity) 조절 (feat. 꼼수)
#include <iostream>
#include <stdio.h>
#include <windows.h>
#include <process.h>
UINT WINAPI ThreadFunction(LPVOID pParam)
{
int nTmp = 0;
while (1)
{
++nTmp;
}
return 0;
}
int main()
{
UINT nThreadId = 0;
HANDLE hThread = (HANDLE)::_beginthreadex(
NULL,
0,
ThreadFunction,
NULL,
0,
&nThreadId);
if (hThread == NULL) {
std::cout << "ERROR: Failed to begin thread." << std::endl;
return 0;
}
for (int i = 0; i < 8; ++i)
{
::SetThreadAffinityMask(hThread, 0x00000001 << i);
::Sleep(5000);
}
::WaitForSingleObject(hThread, INFINITE);
::CloseHandle(hThread);
}
::SetThreadAffinityMask(hThread, 0x00000001 << i); 에서 어떤 cpu를 고를지 정하게 된다.
loop를 8번 돌면서 bitmasking을 해줘서 고르게 된다. 3이 되는 경우에는 101 임으로 core2개를 써서 연산을 하게 된다.
꼼수 관련된것 질문
'Operating System > Windows 시스템 프로그래밍 - 기본' 카테고리의 다른 글
프로세스 관리 (0) | 2023.07.18 |
---|---|
메모리 시스템 (0) | 2023.07.15 |
Win32 파일 입/출력 (0) | 2023.07.10 |
스레드 생성 및 제어 (0) | 2023.07.05 |
기본 이론 (0) | 2023.07.03 |