본문 바로가기
개인공부/윤성우의 열혈 TCP&IP 소켓 프로그래밍

Chapter 20 - Windows에서의 쓰레드 동기화

by 하고싶은건많은놈 2023. 2. 24.

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)
  1. 유저모드 동기화, 커널모드 동기황와 관련된 다음 문장들 중에서 말하는 바가 옳으면 O, 틀리면 X를 표시하자.
  • 유저모드 동기화는 커널모드로의 전환을 수반하지 않는다. 즉, 운영체제 레벨에서 제공되는 기능의 동기화가 아니다. (O)
  • 커널모드 동기화는 운영체제를 통해서 제공되는 기능이므로, 유저모드 동기화에 비해서 많은 기능을 제공한다. (O)
  • 커널모드 동기화 과정에서는 유저모드에서 커널모드로, 다시 커널모드에서 유저모드로의 전환과정이 수반된다는 단점이 있다. (O)
  • 특별한 경우가 아니면 커널모드 동기화를 사용하는 것이 원칙이다. 유저모드 동기화는 커널모드 동기화가 제공되기 이전의 동기화 기법이다. (X)
  1. 본문의 예제 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;
     }
  1. 본문의 예제 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;
     }

댓글