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 : 보안관련 특성 정보, 디폴트 보안설정은 NULL
  • bInitialOwner : 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 상태가 됨
Mutexauto-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);
}

내용 확인문제

  1. 윈도우 운영체제의 유저몯, 커널모드와 관련해서 옳은 것을 모두 고르면?
    • 유저모드는 응용 프로그램이 실행되는 기본모드로, 접근할 수 있는 메모리의 영역에는 제한이 없지만 물리적인 영역으로의 접근은 허용되지 않는다. (X)
    • 응용 프로그램이 실행되는 과정에서는 절대 커널모드로 진입하지 않는다. 응용 프로그램이 실행중인 과정에서는 유저모드로만 동작한다. (X)
    • 윈도우는 메모리의 효율적인 사용을 위해서 유저모드와 커널모드를 각각 별도로 정의하고있다. (X)
    • 응용 프로그램이 실행되는 과정에서도 커널모드로의 변환이 발생할 수 있다. 단, 일단 커널모드로 변환이 되면, 프로세스는 이 상태로 실행을 계속 이어하게된다. (X)
  2. 유저모드 동기화, 커널모드 동기황와 관련된 다음 문장들 중에서 말하는 바가 옳으면 O, 틀리면 X를 표시하자.
    • 유저모드 동기화는 커널모드로의 전환을 수반하지 않는다. 즉, 운영체제 레벨에서 제공되는 기능의 동기화가 아니다. (O)
    • 커널모드 동기화는 운영체제를 통해서 제공되는 기능이므로, 유저모드 동기화에 비해서 많은 기능을 제공한다. (O)
    • 커널모드 동기화 과정에서는 유저모드에서 커널모드로, 다시 커널모드에서 유저모드로의 전환과정이 수반된다는 단점이 있다. (O)
    • 특별한 경우가 아니면 커널모드 동기화를 사용하는 것이 원칙이다. 유저모드 동기화는 커널모드 동기화가 제공되기 이전의 동기화 기법이다. (X)
  3. 본문의 예제 SyncSema_win.cppRead 함수는 임계영역을 빠져나가는데 오랜 시간이 걸리도록 정의가 되어있다. 이에 대한 해결책을 제시하고 실제 예제에 적용해보자.

     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;
     }
    
  4. 본문의 예제 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;
     }