Chapter 20 - Windows에서의 쓰레드 동기화
Chapter 20 - Windows에서의 쓰레드 동기화
20-1 : 동기화 기법의 분류와 CRITICAL_SECTION 동기화
윈도우 운영체제의 연산방식을 이중모드 연산(Dual-mode Operation)이라고 부름
- 유저모드(User mode) : 응용 프로그램이 실행되는 기본모드, 물리적인 영역에의 접근이 허용되지 않으며 접근 가능한 메모리 영역에도 제한이 존재
- 커널모드(Kernel mode) : 운영체제가 실행될 때의 모드, 메모리 및 하드웨어의 접근에 제한이 없음
응용 프로그램 실행과정에서는 유저모드 및 커널모드가 수시로 전환됨
유저모드와 커널몯가 나누어져 잇는 것은 운영체제와 관련된 메모리 영역을 보호하기 위함
쓰레드와 같이 커널 오브젝트의 생성을 동반하는 리소스 생성은 유저모드 > 커널모드 > 유저모드의 변환 과정을 거침
단, 이 때 빈번한 모드 변환은 성능에 영향을 줄 수 있음
유저모드 동기화는 운영체제의 도움 없이 응용 프로그램상에서 진행되며 사용방법이 간단하고 속도가 빠르지만 기능이 제한적임
커널모드 동기화는 제공되는 기능이 더 많고 데드락에 걸리지 않도록 타임아웃 지정이 가능하지만 성능에 제약이 따르며, 서로 다른 프로세스 사이에서의 동기화가 가능
CRITICAL_SECTION
기반 동기화는 유저모드 동기화로 CRITICAL_SECTION
오브젝트를 생성하여 활용함
CS
오브젝트를 다루는 여러가지 함수가 존재
#include <windows.h>
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTION lpCRiticalSEction);
Delete...()
함수는CS
오브젝트가 아닌CS
오브젝트가 사용하던 리소스를 소멸시킴- 뮤텍스와 유사한 기능
CRITICAL_SECTION
을 사용한 예제
// SyncCs_win.cpp
#include <iostream>
#include <windows.h>
#include <process.h>
using namespace std;
#define NUM_THREAD 50
unsigned WINAPI threadInc(void *arg);
unsigned WINAPI threadDes(void *arg);
long long num = 0;
CRITICAL_SECTION cs;
int main(int argc, char *argv[])
{
HANDLE tHandles[NUM_THREAD];
InitializerCriticalSection(&cs);
for (int i = 0; i < NUM_THREAD; i++)
{
if (i % 2)
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
else
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
}
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
DeleteCriticalSection(&cs);
cout << "result : " << num << endl;
return 0;
}
unsigned WINAPI threadInc(void *arg)
{
EnterCriticalSection(&cs);
for (int i = 0; i < 50000000; i++)
num += 1;
LeaveCritialSection(&cs);
return 0;
}
unsigned WINAPI threadDes(void *arg)
{
EnterCriticalSection(&cs);
for (int i = 0; i < 50000000; i++)
num -= 1;
LeaveCritialSection(&cs);
return 0;
}
20-2 : 커널모드 동기화 기법
커널모드 동기화에는 Event
, Semaphore
, Mutex
기법이 존재
Mutex
오브젝트 생성에는 CreateMutex()
함수 사용
#include <windows.h>
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName);
// 성공시 생성된 Mutex 오브젝트의 핸들, 실패시 NULL 반환
lpMutexAttributes
: 보안관련 특성 정보, 디폴트 보안설정은 NULLbInitialOwner
: TRUE 전달시 생성된Mutex
오브젝트는 함수를 호출한 쓰레드의 소유가 되며non-signaled
상태, FALSE 전달시Mutex
오브젝트의 소유자가 존재하지 않으며signaled
상태가 됨lpName
:Mutex
오브젝트에 이름 부여, NULL 전달시 이름 없는 오브젝트 생성
소멸은 CloseHandle()
함수, 획득은 WaitForSingleObject()
함수, 소멸은 ReleaseMutex()
함수 사용
#incldue <windows.h>
BOOL CloseHandle(HANDLE hObject);
// 성공시 TRUE, 실패시 FALSE 반환
BOOL ReleaseMutex(HANDLE hMutex);
// 성공시 TRUE, 실패시 FALSE 반환
Mutex
는 소유시 non-signaled
, 반납시 signaled
상태가 됨
Mutex
는 auto-reset
모드 커널 오브젝트이며 WaitForSingleObject()
함수 호출시 블로킹 상태라면 non-siganeld
, 반환되면 signaled
상태를 뜻함
Mutex
오브젝트 사용 예시
// SyncMutex_win.cpp
#include <iostream>
#include <windows.h>
#include <process.h>
using namespace std;
#define NUM_THREAD 50
unsigned WINAPI threadInc(void *arg);
unsigned WINAPI threadDes(void *arg);
long long num = 0;
sa_handler hMutex;
int main(int argc, char *argv[])
{
HANDLE tHandles[NUM_THREAD];
hMutex = CreateMutex(NULL, FALSE, NULL);
for (int i = 0; i < NUM_THREAD; i++)
{
if (i % 2)
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
else
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
}
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
CloseHandle(hMutex);
cout << "result : " << num << endl;
return 0;
}
unsigned WINAPI threadInc(void *arg)
{
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < 50000000; i++)
num += 1;
ReleaseMutex(hMutex);
return 0;
}
unsigned WINAPI threadDes(void *arg)
{
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < 50000000; i++)
num -= 1;
ReleaseMutex(hMutex);
return 0;
}
Semaphore
오브젝트 기반 동기화도 리눅스와 유사
Semaphore
오브젝트 생성에는 CreateSemaphore()
함수 사용
#include <windows.h>
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName);
// 성공시 생성된 Semaphore 오브젝트의 핸들, 실패시 NULL 반환
lInitialCount
: 세마포어 초기값 지정, 0 이상IMaximumCount
값 미만이어야 함IMaximumCount
: 최대 세마포어 값 지정
소멸은 CloseHandle()
함수, 획득은 WaitForSingleObject()
함수, 소멸은 ReleaseSemaphore()
함수 사용
#include <windows.h>
BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount);
// 성공시 TRUE, 실패시 FALSE 반환
lReleaseCount
: s반납시 증가할 세마포어 값을 지정, 최대 값을 넘길시 증가하지 않고 FALSE 반환lpPreviousCount
: 변경 이전의 세마포어 값 저장을 위한 변수의 주소 값 전달, 불필요시 NULL
Semaphore
오브젝트 사용 예시
// SyncSema_win.cpp
#include <iostream>
#include <windows.h>
#include <process.h>
using namespace std;
unsigned WINAPI Read(void *arg);
unsigned WINAPI Accu(void *arg);
static HANDLE semOne;
static HANDLE semTwo;
static int num;
int main(int argc, char *argv[])
{
HANDLE hThread1, hThread2;
semOne = CreateSemaphore(NULL, 0, 1, NULL);
semTwo = CreateSemaphore(NULL, 1, 1, NULL);
hThread1 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, Accu, NULL, 0, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(semOne);
CloseHandle(semTwo);
return 0;
}
unsigned WINAPI Read(void *arg)
{
for (int i = 0; i < 50000000; i++)
{
cout << "Input num : ";
WaitForSingleObject(semTwo, INFINITE);
cin >> num;
ReleaseSemaphore(semOne, 1, NULL);
}
return 0;
}
unsigned WINAPI threadDes(void *arg)
{
for (int i = 0; i < 50000000; i++)
{
WaitForSingleObject(semOne, INFINITE);
sum += num;
ReleaseSemaphore(semTwo, 1, NULL);
}
cout << "Result : " << sum << endl;
return 0;
}
Event
동기화 오브젝트의 경우 auto-reset
모드와 manual-reset
모드 중 하나를 선택할 수 있음
Event
오브젝트의 생성에는 CreateEvent()
함수를 사용
#include <windows.h>
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPCTSTR lpName);
// 성공시 생성된 Event 오브젝트의 핸들, 실패시 NULL 반환
bManualReset
: TRUE 전달시manual-reset
, FALSE 전달시auto-reset
모드bInitialState
: TRUE 전달시signaled
상태, FALSE 전달시non-signaled
상태
manual-reset
으로 설정시 명시적인 오브젝트 상태의 변경이 필요
#include <windows.h>
BOOL ResetEvent(HANDLE hEvent); // to the non-signaled
BOOL SetEvent(HANDLE hEvent); // to the signaled
// 성공시 TRUE, 실패시 FALSE 반환
Event
오브젝트 사용 예시
// SyncEvent_win.cpp
#include <iostream>
#include <windows.h>
#include <process.h>
using namespace std;
#define STR_LEN 100
unsigned WINAPI NumberOfA(void *arg);
unsigned WINAPI NumberOfOthers(void *arg);
static char str[STR_LEN];
static HANDLE hEvent;
int main(int argc, char *argv[])
{
HANDLE hThread1, hThread2;
hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);
cout << "Input string : ";
cin.getline(str, STR_LEN);
SetEvent(hEvent);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
ResetEvent(hEvent);
CloseHandle(hEvent);
return 0;
}
unsigned WINAPI NumberOfA(void *arg)
{
int cnt = 0;
WaitForSingleObject(hEvent, INFINITE);
for (int i = 0; str[i] != 0; i++)
{
if (str[i] == 'A')
cnt++;
}
cout << "Num of A : " << cnt << endl;
return 0;
}
unsigned WINAPI NumberOfOthers(void *arg)
{
int cnt = 0;
WaitForSingleObject(hEvent, INFINITE);
for (int i = 0; str[i] != 0; i++)
{
if (str[i] != 'A')
cnt++;
}
cout << "Num of others : " << cnt - 1 << endl;
return 0;
}
20-3 : 윈도우 기반의 멀티 쓰레드 서버 구현
윈도우 기반 채팅 서버 예시
// chat_serv_win.cpp
#include <iostream>
#include <unistd.h>
#include <windows.h>
#include <process.h>
using namespace std;
#define BUF_SIZE 100
#define MAX_CLNT 256
unsigned WINAPI HandleClnt(void *arg);
void SendMsg(char *msg, int len);
void ErrorHandling(char* message);
int clntCnt = 0;
SOCKET clntSocks[MAX_CLNT];
HANDLE hMutex;
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAdr, clntAdr;
int clntAdrSz;
HANDLE hThread;
if(argc != 2)
{
cout << "Usage : " << argv[0] << " <port>\n";
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error");
hMutex = CreateMutex(NULL, FALSE, NULL);
hServSock = socket(PF_INET, SOCK_STREAM, 0);
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
servAdr.sin_port = htons(atoi(argv[1]));
if (::bind(hServSock, (sockaddr*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
ErrorHandling("bind() error");
if (::listen(hServSock, 5) == SOCKET_ERROR)
ErrorHandling("listen() error");
while (1)
{
clntAdrSz = sizeof(clntAdr);
hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSz);
WaitForSingleObject(hMutex, INFINITE);
clntSocks[clntCnt++] = hClntSock;
ReleaseMutex(hMutex);
hThread = (HANDLE)_beginthreadex(NULL, 0, HandleClnt, (void*)&hClntSock, 0, NULL);
cout << "Connected client IP : " << inet_ntoa(clntAdr.sin_addr) << endl;
}
closesocket(hServSock);
WSACleanup();
return 0;
}
unsigned WINAPI HandleClnt(void *arg)
{
SOCKET hClntSock = *((SOCKET*)arg);
int strLen = 0;
char msg[BUF_SIZE];
while ((strLen = recv(hClntSock, msg, sizeof(msg), 0)) != 0)
SendMsg(msg, strLen);
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < clntCnt; i++) // remove disconnected client
{
if (hClntSock == clntSocks[i])
{
while (i++ < clntCnt - 1)
clntSocks[i] = clntSocks[i + 1];
break;
}
}
clntCnt--;
ReleaseMutex(hMutex);
closesocket(hClntSock);
return NULL;
}
void SendMsg(char *msg, int len) // send to all
{
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < clntCnt; i++)
send(clntSocks[i], msg, len, 0);
ReleaseMutex(hMutex);
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
윈도우 기반 채팅 클라이언트 예시
// chat_clnt_win.cpp
#include <iostream>
#include <unistd.h>
#include <windows.h>
#include <process.h>
using namespace std;
#define BUF_SIZE 100
#define NAME_SIZE 20
unsigned WINAPI SendMsg(void *arg);
unsigned WINAPI RecvMsg(void *arg);
void ErrorHandling(char* message);
char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hSock;
SOCKADDR_IN servAdr;
HANDLE hSndThread, hRcvThread;
if(argc != 4)
{
cout << "Usage : " << argv[0] << " <IP> <port> <name>\n";
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error");
sprintf(name, "[%s]", argv[3]);
hSock = socket(PF_INET, SOCK_STREAM, 0);
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.s_addr = inet_addr(argv[1]);
servAdr.sin_port = htons(atoi(argv[2]));
if (::connect(hSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
ErrorHandling("connect() error");
hSndThread = (HANDLE)_beginthreadex(NULL, 0, SendMsg, (void*)&hSock, 0, NULL);
hRcvThread = (HANDLE)_beginthreadex(NULL, 0, RecvMsg, (void*)&hSock, 0, NULL);
WaitForSingleObject(hSndThread, INFINITE);
WaitForSingleObject(hRcvThread, INFINITE);
closesocket(hSock);
WSACleanup();
return 0;
}
unsigned WINAPI SendMsg(void *arg)
{
SOCKET hSock = *((SOCKET*)arg);
char nameMsg[NAME_SIZE + BUF_SIZE];
while (1)
{
fgets(msg, BUF_SIZE, stdin);
if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n"))
{
close(sock);
exit(0);
}
sprintf(nameMsg, "%s %s", name, msg);
send(hSock, nameMsg, strlen(nameMsg), 0);
}
return NULL;
}
unsigned WINAPI RecvMsg(void *arg)
{
SOCKET hSock = *((SOCKET*)arg);
char nameMsg[NAME_SIZE + BUF_SIZE];
int strLen;
while (1)
{
strLen = recv(hSock, nameMsg, NAME_SIZE + BUF_SIZE - 1, 0);
if (strLen == -1)
return -1;
nameMsg[strLen] = 0;
fputs(nameMsg, stdout);
}
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
내용 확인문제
- 윈도우 운영체제의 유저몯, 커널모드와 관련해서 옳은 것을 모두 고르면?
- 유저모드는 응용 프로그램이 실행되는 기본모드로, 접근할 수 있는 메모리의 영역에는 제한이 없지만 물리적인 영역으로의 접근은 허용되지 않는다. (X)
- 응용 프로그램이 실행되는 과정에서는 절대 커널모드로 진입하지 않는다. 응용 프로그램이 실행중인 과정에서는 유저모드로만 동작한다. (X)
- 윈도우는 메모리의 효율적인 사용을 위해서 유저모드와 커널모드를 각각 별도로 정의하고있다. (X)
- 응용 프로그램이 실행되는 과정에서도 커널모드로의 변환이 발생할 수 있다. 단, 일단 커널모드로 변환이 되면, 프로세스는 이 상태로 실행을 계속 이어하게된다. (X)
- 유저모드 동기화, 커널모드 동기황와 관련된 다음 문장들 중에서 말하는 바가 옳으면 O, 틀리면 X를 표시하자.
- 유저모드 동기화는 커널모드로의 전환을 수반하지 않는다. 즉, 운영체제 레벨에서 제공되는 기능의 동기화가 아니다. (O)
- 커널모드 동기화는 운영체제를 통해서 제공되는 기능이므로, 유저모드 동기화에 비해서 많은 기능을 제공한다. (O)
- 커널모드 동기화 과정에서는 유저모드에서 커널모드로, 다시 커널모드에서 유저모드로의 전환과정이 수반된다는 단점이 있다. (O)
- 특별한 경우가 아니면 커널모드 동기화를 사용하는 것이 원칙이다. 유저모드 동기화는 커널모드 동기화가 제공되기 이전의 동기화 기법이다. (X)
-
본문의 예제
SyncSema_win.cpp
의Read
함수는 임계영역을 빠져나가는데 오랜 시간이 걸리도록 정의가 되어있다. 이에 대한 해결책을 제시하고 실제 예제에 적용해보자.unsigned WINAPI Read(void *arg) { int temp; for (int i = 0; i < 50000000; i++) { cout << "Input num : "; cin >> temp; WaitForSingleObject(semTwo, INFINITE); num = temp; ReleaseSemaphore(semOne, 1, NULL); } return 0; }
-
본문의 예제
SyncEvent_win.cpp
를 세마포어 기반의 동기화 기법을 적용해서 동일한 실행결과를 보이도록 재구현해보자.// SyncEventtoSema_win.cpp #include <iostream> #include <windows.h> #include <process.h> using namespace std; #define STR_LEN 100 unsigned WINAPI NumberOfA(void *arg); unsigned WINAPI NumberOfOthers(void *arg); static HANDLE semOne; static HANDLE semTwo; static char str[STR_LEN]; int main(int argc, char *argv[]) { HANDLE hThread1, hThread2; semOne = CreateSemaphore(NULL, 0, 1, NULL); semTwo = CreateSemaphore(NULL, 1, 1, NULL); hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL); hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL); cout << "Input string : "; cin.getline(str, STR_LEN); ReleaseSemaphore(semTwo, 1, NULL); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(semOne); CloseHandle(semTwo); return 0; } unsigned WINAPI NumberOfA(void *arg) { int cnt = 0; WaitForSingleObject(semTwo, INFINITE); for (int i = 0; str[i] != 0; i++) { if (str[i] == 'A') cnt++; } cout << "Num of A : " << cnt << endl; ReleaseSemaphore(semOne, 1, NULL); return 0; } unsigned WINAPI NumberOfOthers(void *arg) { int cnt = 0; WaitForSingleObject(semOne, INFINITE); for (int i = 0; str[i] != 0; i++) { if (str[i] != 'A') cnt++; } cout << "Num of others : " << cnt - 1 << endl; ReleaseSemaphore(semTwo, 1, NULL); return 0; }