책정리/열혈 TCP,IP

20장 Windows에서의 쓰레드 동기화

GONII 2015. 8. 5. 16:19
  • 동기화 기법의 분류와 CRITICAL_SECTION 동기화

    유저모드(User mode)와 커널모드(Kernel mode)

    윈도우 운영체제의 연산방식(프로그램 실행방식)을 가리켜 '이중모드 연산(Dual-mode Operation)'방식이라 한다. 이는 연산을 하는데 있어서 윈도우에 두 가지 모드가 존재함을 뜻한다.

유저모드

응용프로그램이 실행되는 기본모드로, 물리적인 영역으로의 접근이 허용되지 않으며, 접근할 수 있는 메모리의 영역에도 제한이 따른다

커널모드

운영체제가 실행될 때의 모드로 메모리뿐만 아니라 하드웨어의 접근에도 제한이 따르지 않는다.

커널은 운영체제의 핵심모듈을 의미하므로, 이를 다음과 같이 단순히 정의할 수도 있다.

유저모드

응용프로그램의 실행모드

커널모드

운영체제의 실행모드

응용프로그램의 실행과정에서 윈도우 운영체제가 항상 유저모드에만 머무는 것이 아니라, 유저모드와 커널모드를 오가며 실행하게 된다. 예를 들어 쓰레드의 생성요청은 응용프로그램에서 호출되지만 쓰레드를 실제로 생성하는 것은 운영체제이다. 따라서 쓰레드의 생성을 위해서는 커널모드로의 전환이 불가피하다.

이렇게 두 가지 모드를 정의하고 있는 이유는 안전성을 높이기 위함이다. 응용프로그램상에서의 잘못된 연산은 운영체제의 손상 및 다양한 리소스들의 손상으로 이어질 수 있다.

쓰레드와 같이 커널 오브젝트의 생성을 동반하는 리소스의 생성을 위해서는 '유저모드->커널모드->유저모드' 와 같이 모드 변환의 과정을 거쳐야 한다.

유저모드에서 커널모드로의 전환은 리소스의 생성을 위한 것이고, 커널모드에서 유저모드로의 재 전환은 응용프로그램의 나머지 부분을 이어서 실행하기 위한 것이다. 이렇듯 리소스의 생성뿐만 아니라 커널 오브젝트와 관련된 모든 일은 커널 모드에서 진행되기 때문에 모드의 변환 역시 시스템 성능에 영향을 줄 수 있다.

유저모드 동기화

유저모드 동기화의 가장 큰 장점은 "속도가 빠르다"이다.

커널 모드로의 전환이 불필요하기 때문에 다른 동기화 기법에 비해 빠를 수 밖에 없다.

커널모드 동기화

커널모드 동기화의 장점은 다음과 같다

  • 유저모드 동기화에 비해 제공된느 기능이 많다.
  • Dead-lock에 걸리지 않도록 타임아웃의 지정이 가능하다.

커널모드에서 동기화를 진행하면 서로 다른 프로세스에 포함되어 있는 두 쓰레드간의 동기화도 가능하다.

CRITICAL_SECTION 기반의 동기화

CRITICAL_SECTION 오브젝트라는 것을 생성하여 이를 동기화에 활용한다.

임계영역의 진입을 위해 CRITICAL_SECTION 오브젝트라는 열쇠를 얻어야 하고 임계영역을 빠져나갈 때에는 CRITICAL_SECTION 오브젝트를 반납해야 한다.

#include <windows.h>

void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

  • lpCriticalSection

    Init : 초기화 할 CRITICAL_SECTION 오브젝트의 주소 값 전달

    Del : 해제할 CRITICAL_SECTION 오브젝트의 주소 값 전달

DeleteCriticalSection 함수는 사용하던 리소스를 소멸시키는 함수이다.

#include <windows.h>

void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

  • lpCriticalSection

    획득(소유) 및 반납할 CRITICAL_SECTION 오브젝트의 주소 값 전달

  • 예제 SyncCS_win.c

#include <stdio.h>

#include <windows.h>

#include <process.h>

   

#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];

int i;

   

InitializeCriticalSection(&cs);

for( 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);

printf("result : %11d\n", num);

   

return 0;

}

unsigned WINAPI threadInc(void* arg)

{

int i;

EnterCriticalSection(&cs);        

for( i = 0 ; i < 100 ; i++ )

{

printf("%d\n", num);

num += 1;

}        

LeaveCriticalSection(&cs);

return 0;

}

unsigned WINAPI threadDes(void* arg)

{

int i;

EnterCriticalSection(&cs);

for( i = 0 ; i < 100 ; i++ )

{

printf("%d\n", num);

num -= 1;

}

LeaveCriticalSection(&cs);

return 0;

}

  • 커널모드 동기화 기법

    대표적인 커널모드 동기화 기법에는 Event, Semaphore, Mutex라는 커널 오브젝트 기반의 동기화가 있다.

    Mutex(Mutual Exclusion) 오브젝트 기반 동기화

#include <windows.h>

HANDLE CreateMutex(

LPSECURITY_ATTRIBUTES lpMutexAttributes,

BOOL bInitialOwner,

LPCWSTR lpName

);
// 성공 : 생성된 Mutex 오브젝트의 핸들 반환
// 실패 : NULL 반환

  • LPSECURITY_ATTRIBUTES lpMutexAttributes

    보안관련 특성 정보의 전달, 디폴트 설정은 NULL

  • BOOL bInitialOwner

    TRUE전달 시, 생성되는 Mutex오브젝트는 이 함수를 호출한 쓰레드의 소유가 되면서 non-signaled 상태가 된다. FALSE 전달 시 생성되는 Mutex 오브젝트는 소유자가 존재하지 않으며 signaled 상태로 생성된다.

  • LPCWSTR lpName

    Mutex 오브젝트에 이름을 부여할 때 사용된다. NULL 전달 하면 이름 없는 Mutex 오브젝트 생성

Mutex 오브젝트는 소유자가 없는 경우에 signaled 상태가 된다. 따라서 이러한 특성을 이용해서 동기화를 진행한다. 그리고 Mutex는 커널 오브젝트이기 때문에 다음 함수 호출을 통해 소멸이 이뤄진다.

#include <windows.h>

BOOL CloseHandle(HANDLE hObject);
// 성공 : TRUE 반환
// 실패 : FALSE 반환

위 함수는 커널 오브젝트를 소멸하는 함수이기 때문에 Semaphore와 Event의 소멸에도 사용된다.

획득은 WaitForSingleObject의 함수호출을 통해 이뤄진다. 반납에 관련된 함수는 다음과 같다.

#include <windows.h>

BOOL ReleaseMutex(HANDLE hMutex);
// 성공 : TRUE
// 실패 : FALSE

  • hMutex

    반납할, 소유를 해제할 Mutex 오브젝트의 핸들 전달

Mutex는 소유되었을 때 non-signaled상태가 되고

반납되었을 때 signaled 상태가 된다.

소유 여부를 확인할 때는 WaitForSingleObject 함수를 이용할 수 있다. 이 함수의 호출결과는 다음 두 가지 형태로 정리가 된다.

호출 후 블로킹 상태

Mutex 오브젝트가 다른 쓰레드에게 소유되어 현재 non-signaled 상태에 놓인 상황

호출 후 반환된 상태

Mutex 오브젝트의 소유가 해제되었거나 소유되지 않아서 signaled 상태에 놓여있는 상황

Mutex는 WaitForSingleObject 함수가 반환될 때 자동으로 non-signaled 상태가 되는 'auto-reset모드' 커널 오브젝트이다. 따라서 WaitForSingleObject 함수가 결과적으로 Mutex를 소유할 때 호출하는 함수가 된다. 그러므로 Mutex 기반의 임계영역 보호를 위함 코드는 다음과 같이 구성된다.

WaitForSingleObject(hMutex, INFINITE);

// 임계영역의 시작

// ...

// 임계영역의 끝

ReleaseMutex(hMutex);

WaitForSingleObject 함수는 Mutex를 non-signaled 상태로 만들어서 임계영역으로의 접근을 막기 때문에 임계영역의 진입로 역할을 할 수 있다. 반면 ReleaseMutex 함수는 Mutex 오브젝트를 다시 signaled 상태로 만들기 때문에 임계영역의 출구역할을 할 수 있다.

  • 예제 SyncMutex_win.c

#include <stdio.h>

#include <windows.h>

#include <process.h>

   

#define NUM_THREAD 50

   

unsigned WINAPI threadInc(void* arg);

unsigned WINAPI threadDes(void* arg);

   

long long num = 0;

HANDLE hMutex;

   

int main(int argc, char* argv[])

{

HANDLE tHandles[NUM_THREAD];

int i;

   

hMutex = CreateMutex(NULL, FALSE, NULL);

for( 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);

printf("result : %11d\n", num);

   

return 0;

}

   

unsigned WINAPI threadInc(void* arg)

{

int i;

WaitForSingleObject(hMutex, INFINITE);

for( i = 0 ; i < 50000 ; i++ )

num += 1;

ReleaseMutex(hMutex);

return 0;

}

unsigned WINAPI threadDes(void* arg)

{

int i;

WaitForSingleObject(hMutex, INFINITE);

for( i = 0 ; i < 50000 ; i++ )

num -= 1;

ReleaseMutex(hMutex);

return 0;

}

Semaphore 오브젝트 기반 동기화

윈도우의 Semaphore 오브젝트 기반 동기화 역시 리눅스의 세마포어와 유사하다. 둘 다 세마포어 값(Semaphore Value)이라 불리는 정수를 기반으로 동기화가 이뤄지고, 이 값이 0보다 작아질 수 없다는 특징도 동일하다.

Semaphore 오브젝트의 생성에 사용된느 함수는 다음과 같다.

#include <windows.h>

HANDLE CreateSemaphore(

LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,

LONG lInitialCount,

LONG lMaximumCount,

LPCWSTR lpName

);

  • lpSemaphoreAttributes

    보안관련 정보의 전달, 디폴트 NULL 전달

  • lInitialCount

    세마포어의 초기 값 지정, lMaximumCount에 전달된 값보다 크면 안되고, 0이상이여야 한다

  • lMaximumCount

    최대 세마포어 값을 지정. 1을 전달하면 세마포어 값이 0 또는 1이 되어 바이너리 세마포어가 구성된다.

  • lpName

    Semaphore 오브젝트에 이름을 부여할 때 사용된다. NULL 전달 시 이름 없는 Semaphore 오브젝트 생성

세마포어 값이 0인 경우 non-signaled 상태가 되고, 0보다 큰 경우 signaled 상태가 되는 특성을 이용해서 동기화가 진행된다. lInitalCount에 0이 전달되면, non-signaled 상태의 Semaphore 오브젝트가 생성된다. 또한 매개변수 lMaximumCount에 3을 전달하면 세마포어의 최대 값은 3이기 때문에 세 개의 쓰레드가 동시에 임계영역에 진입하는 유형의 동기화도 가능하다.

Semaphore 오브젝트의 반납에 사용되는 함수는 다음과 같다

#include <windows.h>

BOOL ReleaseSemaphore(

HANDLE hSemaphore,

LONG lReleaseCount,

LPLONG lpPreviousCount

);
// 성공 : TRUE

// 실패 : FALSE

  • hSemaphore

    반납할 Semaphore 오브젝트의 핸들 전달

  • lReleaseCount

    반납할 세마포어 값의 증가를 의미하는데, 이 매개변수를 통해서 증가되는 값의 크기를 지정할 수 있다. 이로 인해서 세마포어의 최대값을 넘어서게 되면 값은 증가하지 않고 FALSE가 반환된다.

  • lpPreviousCount

    변경 이전의 세마포어 값 저장을 위한 변수의 주소 값 전달, 불필요하다면 NULL 전달

Semaphore 오브젝트는 세마포어 값이 0보다 큰 경우 signaled 상태가 되고, 0인 경우에 non-signaled 상태가 되기 때문에 WaitForSingleObject 함수가 호출되면 세마포어 값이 0보다 큰 경우에 반환을 한다. 그리고 이렇게 반환이 되면 세마포어 값을 1 감소시키면서 non-signaled 상태가 되게 한다. 따라서 다음의 형태로 임계영역의 보호가 가능하다.

WaitForSingleObject(hSemaphore, INFINITE);
// 임계영역 시작
// ........
// 임계영역 끝
ReleaseSemaphore(hSemaphore, 1, NULL);

  • 예제 SyncSema_win.c

#include <stdio.h>

#include <windows.h>

#include <process.h>

   

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)

{

int i;

for( i = 0 ; i < 5 ; i++ )

{

fputs("Input num: ", stdout);

WaitForSingleObject(semTwo, INFINITE);

scanf("%d", &num);

ReleaseSemaphore(semOne, 1, NULL);

}

   

return 0;

}

unsigned WINAPI Accu(void* arg)

{

int sum = 0, i;

for( i = 0 ; i < 5 ; i++ )

{

WaitForSingleObject(semOne, INFINITE);

sum += num;

ReleaseSemaphore(semTwo, 1, NULL);

}

printf("Result: %d\n", sum);

return 0;

}

Event 오브젝트 기반 동기화

이벤트 오브젝트는 생성과정에서 auto-reset모드와 manual-reset 모드 중 하나를 선택할 수 있다.

#include <windows.h>

HANDLE CreateEvent(

LPSECURITY_ATTRIBUTES lpEventAttributes,

BOOL bManualReset,

BOOL bInitialState,

LPCWSTR lpName

);
// 성공 : 생성된 Event 오브젝트 핸들
// 실패 : NULL 반환

  • lpEventAttributes

    보안관련 정보의 전달, 디폴트 보안 설정 시 NULL 전달

  • bManualReset

    TRUE 전달 시 manual-reset 모드 Event

    FALSE 전달 시 auto-reset 모드 Event 오브젝트 생성

  • bInitialState

    TRUE 전달시 signaled 상태의 Event오브젝트 생성

    FALSE 전달 시 non-signaled 상태의 Event오브젝트 생성

  • lpName

    Event오브젝트에 이름을 부여할 때 사용, NULL 전달하면 이름없는 Event 오브젝트 생성

manual-reset 모드의 Event 오브젝트가 생성되면 WaitForSingleObject 함수가 반환을 한다고 해서 non-signaled 상태로 되돌려지지 않는다. 따라서 이러한 경우에는 다음 두 함수를 이용해서 명시적으로 오브젝트의 상태를 변경해야 한다.

#include <windows.h>

BOOL ResetEvent(HANDLE hEvent); // to the non-signaled
BOOL SetEvent(HANDLE hEvent); // to the signaled
// 성공 : TRUE
// 실패 : FALSE

  • 예제 SyncEvent_win.c

#include <stdio.h>

#include <windows.h>

#include <process.h>

#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);

   

fputs("Input string: ", stdout);

fgets(str, STR_LEN, stdin);

SetEvent(hEvent);

WaitForSingleObject(hThread1, INFINITE);

WaitForSingleObject(hThread2, INFINITE);

ResetEvent(hEvent);

CloseHandle(hEvent);

return 0;

}

   

unsigned WINAPI NumberOfA(void* arg)

{

int i, cnt = 0;

WaitForSingleObject(hEvent, INFINITE);

for( i = 0 ; str[i] != 0 ; i++ )

{

if( str[i] == 'A' )

cnt++;

}

printf("Num of A: %d\n", cnt);

return 0;

}

unsigned WINAPI NumberOfOthers(void* arg)

{

int i, cnt = 0;

WaitForSingleObject(hEvent, INFINITE);

for( i = 0 ; str[i] != 0 ; i++ )

{

if( str[i] != 'A' )

cnt++;

}

   

printf("Num of others: %d\n", cnt-1);

return 0;

}

  • 윈도우 기반의 멀티 쓰레드 서버 구현
    • 예제 chat_seerv_win.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <windows.h>

#include <process.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUF_SIZE 100

#define MAX_CLNT 256

   

unsigned WINAPI HandleClnt(void* arg);

void SendMsg(char* msg, int len);

void error_handling(char* msg);

   

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 )

{

printf("Usage : %s <port>\n", argv[0]);

exit(1);

}*/

if( WSAStartup(MAKEWORD(2,2), &wsaData) != 0 )

error_handling("WSAStartup() error");

   

hMutex = CreateMutex(NULL, FALSE, NULL);

// socket

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(9090);

   

// bind

if( bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR )

error_handling("bind() error");

   

// listen

if( listen(hServSock, 5) == SOCKET_ERROR )

error_handling("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);

printf("Connected clint IP:%s\n", inet_ntoa(clntAdr.sin_addr));

}

   

closesocket(hServSock);

WSACleanup();

return 0;

}

   

unsigned WINAPI HandleClnt(void* arg)

{

SOCKET hClntSock = *((SOCKET*)arg);

int strLen = 0, i;

char msg[BUF_SIZE];

   

while( (strLen = recv(hClntSock, msg, sizeof(msg), 0)) != 0 )

SendMsg(msg, strLen);

   

WaitForSingleObject(hMutex, INFINITE);

for( 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 0;

}

void SendMsg(char* msg, int len)

{

int i;

WaitForSingleObject(hMutex, INFINITE);

for( i = 0 ; i < clntCnt ; i++ )

send(clntSocks[i], msg, len, 0);

   

ReleaseMutex(hMutex);

}

void error_handling(char* msg)

{

fputs(msg, stderr);

fputc('\n', stderr);

exit(1);

}

  • 예제 chat_clnt_win.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <windows.h>

#include <process.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUF_SIZE 100

#define NAME_SIZE 20

   

unsigned WINAPI SendMsg(void* arg);

unsigned WINAPI RecvMsg(void* arg);

void error_handling(char* msg);

   

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 )

{

printf("Usage : %s <IP> <PORT> <NAME>\n", argv[0]);

exit(1);

}

   

if( WSAStartup(MAKEWORD(2,2), &wsaData) != 0 )

error_handling("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)

error_handling("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") )

{

closesocket(hSock);

exit(0);

}

sprintf(nameMsg, "%s %s", name, msg);

send(hSock, nameMsg, strlen(nameMsg), 0);

}

   

return 0;

}

unsigned WINAPI RecvMsg(void* arg)

{

int 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 error_handling(char* msg)

{

fputs(msg, stderr);

fputc('\n', stderr);

exit(1);

}

 

반응형