본문 바로가기

High Level Technique/Window System

생산자/소비자 모델

생산자/소비자 모델


생산자/소비자 모델을 풀어 말하면 실행 순서에 있어서의 동기화라고 할 수 있다.


가령 빵을 만드는 생산자가 빵을 구워서 테이블에 두면 소비자는 빵을 사먹을 수 있다. 이 순서가 바뀌어 소비자가 비어있는 테이블에 빵을 찾게 되고 그 뒤 생산자가 빵을 놓게 된다면 적절하지 못한 순서가 된 것이다. 실행 순서가 동기화 되지 못한것이다.


실행 순서에 있어서 동기화를 생산자/소비자 모델이라 부르는 이유이다.


이를 바탕으로 한 쓰레드가 문자를 입력 받고 출력하는 역할을 모두 실행한다면 출력속도가 입력속도를 따라가지 못하는 경우가 발생하게 되고 어느 순간 문자열이 손실될 것입니다.


위와 같은 현상을 방지하고자 두개의 쓰레드를 이용합니다. A 쓰레드는 입력만 받고 B 쓰레드는 출력만 하는 것이죠.

A와 B 쓰레드 사이에 버퍼를 두고 입출력 속도에 상관없이 독립적으로 실행하게 합니다.



Event 동기화


뮤텍스, 세마포어와 마찬가지로 CreateEvent()로 이벤트 오브젝트를 생성합니다. 마찬가지로 소멸시에 CloseHandle()을 이용하면 됩니다.


쓰레드나 프로세스 커널 오브젝트의 경우 Non-Signaled 상태로 생성되고 종료될 경우 Signaled 상태로 자동으로 변경됩니다. 하지만 이벤트 오브젝트는 자동으로 Signaled 상태가 되지 않습니다.


Non-Signaled 상태의 이벤트 오브젝트가 WaitForSingleObject()가 호출되면 Blocking 상태가 됩니다. 하지만 Signaled 상태가 되면 WaitForSingleObject() 함수를 반환합니다.


이벤트 오브젝트가 Signaled 상태가 되어 Blocking 상태에서 빠져나와 Signaled 상태이면 Manual-Rest Mode라고 하고 Non-Signaled 이면 Auto-Rest Mode라고 합니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
 
TCHAR string[100];
HANDLE hEvent;
 
unsigned int WINAPI OutputThread(LPVOID lpParam)
{
    WaitForSingleObject(hEvent, INFINITE);
 
    _tprintf(_T("Output string: %s \n"), string);
 
    return 0;
}
 
int _tmain(int argc, TCHAR* argv[])
{
    HANDLE hThread;
    DWORD dwThreadID;
 
    hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
 
    if (hEvent == NULL)
    {
        printf("Event Ojbect creation error \n");
        return -1;
    }
 
    hThread = (HANDLE)_beginthreadex(NULL0, OutputThread, NULL0, (unsigned*)&dwThreadID);
 
    if (hThread == NULL)
    {
        printf("Thread creation error \n");
        return -1;
    }
 
    printf("Insert string: ");
    _fgetts(string30, stdin);
 
    SetEvent(hEvent);
 
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hEvent);
    CloseHandle(hThread);
 
    return 0;
}
cs


main 함수에서 _beginthreadex()를 통해서 OutputThread 쓰레드를 실행시킨다. 하지만 11라인에서 WaitForSingleObject()를 통해서 Signaled 상태가 될 때까지 기다린다.


23라인에서 CreateEvent()의 2번째 인자 값을 보면 TRUE로 되어잇는데 이것은 Manual-Rest Mode를 뜻하고 3번째 인자 값은 FALSE로 non-signaled 상태를 말한다.


40라인에서 문자열을 입력한 뒤에 42라인 SetEvent()에서 Event를 Signaled 상태로 변경한다.




이번에는 쓰레드를 하나 더 추가해서 해당 길이를 출력하는 것을 추가로 만들어 보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
 
TCHAR string[100];
HANDLE hEvent;
 
unsigned int WINAPI OutputThread(LPVOID lpParam)
{
    WaitForSingleObject(hEvent, INFINITE);
 
    _tprintf(_T("Output string: %s \n"), string);
 
    return 0;
}
 
unsigned int WINAPI CountThread(LPVOID lpParam)
{
    WaitForSingleObject(hEvent, INFINITE);
    
    _tprintf(_T("Length: %d \n"), _tcslen(string- 1);
 
    return 0;
}
 
int _tmain(int argc, TCHAR* argv[])
{
    HANDLE hThread[2];
    DWORD dwThreadID[2];
 
    hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
 
    if (hEvent == NULL)
    {
        printf("Event Ojbect creation error \n");
        return -1;
    }
 
    hThread[0= (HANDLE)_beginthreadex(NULL0, OutputThread, NULL0, (unsigned*)&dwThreadID[0]);
    hThread[1= (HANDLE)_beginthreadex(NULL0, CountThread, NULL0, (unsigned*)&dwThreadID[1]);
 
    if (hThread[0== NULL || hThread[1== NULL)
    {
        printf("Thread creation error \n");
        return -1;
    }
 
    printf("Insert string: ");
    _fgetts(string30, stdin);
 
    SetEvent(hEvent);
 
    WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
 
    CloseHandle(hEvent);
    CloseHandle(hThread[0]);
    CloseHandle(hThread[1]);
        
    return 0;
}
cs


32라인을 보면 CreateEvent()의 2번째 인자 값이 FALSE로 되어있다. 이것은 Auto-Rest Mode로 한다는 말이다. Auto-Rest Mode는 자동으로 Non-Signaled 상태가 되므로 첫번째 쓰레드는 정상적으로 작동하지만 두번째 쓰레드는 WaitForSingleObject()에서 빠져나오지 못하게 된다. 


출력결과는 아래와 같다.





32라인의 CreateEvent() 2번째 인자 값을 TRUE로 변경해 보자.





위 2개의 그림 중 첫번째 그림이 생각하는 것과 동일한 결과이다. 하지만 계속해서 정상적인 출력을 볼 수 없다.


순서대로 실행을 했다면 말이다. 하지만 52라인에서 SetEvent()를 통해서 Signaled로 바꾸면서 두개의 쓰레드는 모두 Signaled로 바뀌게 되고 모두 실행을 하게 되는 것이다.



이벤트 + 뮤텍스 동기화


위 이벤트 동기화 예제에서 실행이 정상적으로 이루어지지 않는다는 것을 봤다. 이를 해결하기 위해서 이벤트와 뮤텍스를 모두 사용해야 한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
 
typedef struct _SynchString
{
    TCHAR string[100];
    HANDLE hEvent;
    HANDLE hMutex;
 
} SynchString;
 
SynchString gSynString;
 
unsigned int WINAPI OutputThread(LPVOID lpParam)
{
    WaitForSingleObject(gSynString.hEvent, INFINITE);
    WaitForSingleObject(gSynString.hMutex, INFINITE);
 
    _tprintf(_T("Output string: %s \n"), gSynString.string);
 
    ReleaseMutex(gSynString.hMutex);
 
    return 0;
}
 
unsigned int WINAPI CountThread(LPVOID lpParam)
{
    WaitForSingleObject(gSynString.hEvent, INFINITE);
    WaitForSingleObject(gSynString.hMutex, INFINITE);
 
    _tprintf(_T("Length: %d \n"), _tcslen(gSynString.string- 1);
 
    ReleaseMutex(gSynString.hMutex);
 
    return 0;
}
 
int _tmain(int argc, TCHAR* argv[])
{
    HANDLE hThread[2];
    DWORD dwThreadID[2];
 
 
    gSynString.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    gSynString.hMutex = CreateMutex(NULL, FALSE, NULL);
 
    if (gSynString.hEvent == NULL || gSynString.hMutex == NULL)
    {
        printf("kernel object creation error \n");
        return -1;
    }
 
    hThread[0= (HANDLE)_beginthreadex(NULL0, OutputThread, NULL0, (unsigned*)&dwThreadID[0]);
    hThread[1= (HANDLE)_beginthreadex(NULL0, CountThread, NULL0, (unsigned*)&dwThreadID[1]);
 
    if (hThread[0== NULL || hThread[1== NULL)
    {
        printf("Thread creation error \n");
        return -1;
    }
 
    printf("Insert string: ");
    _fgetts(gSynString.string30, stdin);
 
    SetEvent(gSynString.hEvent);
 
    WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
 
    CloseHandle(gSynString.hEvent);
    CloseHandle(gSynString.hMutex);
    CloseHandle(hThread[0]);
    CloseHandle(hThread[1]);
 
    return 0;
}
cs



책의 출력결과를 보면 마지막에 문자열에 대한 길이가 출력이되는데 아무리 해도 문자열 길이가 중간에 출력이 된다.


출판사에서 제공하는 소스코드를 사용해도 마찬가지.


처음 출력에는 예상대로 나왔지만 몇 차례 계속 진행을 해보니 그냥 Event 동기화와 다를게 없었다.


이 부분은 나중에 더 공부해서 알아보도록 해야겠다.



Waitable Timer 동기화


정해진 시간이 지나면 자동으로 signaled 상태가 되도록 하는 것을 Waitable Timer 동기화라 한다.




Manual-Rest Timer


CreateWaitableTimer()를 이용해 타이머 오브젝트를 생성합니다. CreateEvent()와 비교했을 때 Signaled나 Non-Signaled 상태를 설정하는 인자 값이 존재하지 않습니다.

Timer 자체가 일정 시간이 되면 자동으로 Signaled 상태가 되기 때문입니다.


따라서 타이머 오브젝트를 생성하면 Non-Signaled 상태가 됩니다.


CreateWaitableTimer()로 타이머 오브젝트를 생성하고 나면 SetWiatableTimer()로 시간을 설정해 주어야 합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#define _WIN32_WINNT 0x0400
 
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
 
int _tmain(int argc, TCHAR* argv[])
{
    HANDLE hTimer = NULL;
    LARGE_INTEGER liDueTime;
 
    liDueTime.QuadPart = -100000000;
 
    hTimer = CreateWaitableTimer(NULL, FALSE, _T("WaitableTimer"));
 
    if (!hTimer)
    {
        printf("CreateWaitableTimer failed %d \n", GetLastError());
        return -1;
    }
 
    printf("Waiting for 10 seconds... \n");
 
    SetWaitableTimer(hTimer, &liDueTime, 0NULLNULL, FALSE);
 
    WaitForSingleObject(hTimer, INFINITE);
    printf("Timer was signaled \n");
    MessageBeep(MB_ICONEXCLAMATION);
 
    return 0;
}
cs


10초 후에 MessageBeep()가 실행되는 소스코드 입니다. WaitForSingleObject()에서 Timer가 Signaled 상태가 될 때까지 기다리다가 Signaled 상태가 되면 빠져나와 MessageBeep()를 실행시키게 됩니다.



Periodic Timer


주기적으로 작동하는 타이머를 말합니다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#define _WIN32_WINNT 0x0400
 
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
 
int _tmain(int argc, TCHAR* argv[])
{
    HANDLE hTimer = NULL;
    LARGE_INTEGER liDueTime;
 
    liDueTime.QuadPart = -100000000;
 
    hTimer = CreateWaitableTimer(NULL, FALSE, _T("WaitableTimer"));
 
    if (!hTimer)
    {
        printf("CreateWaitableTimer failed %d \n", GetLastError());
        return -1;
    }
 
    printf("Waiting for 10 seconds... \n");
 
    SetWaitableTimer(hTimer, &liDueTime, 5000NULLNULL, FALSE);
    while (1)
    {
        WaitForSingleObject(hTimer, INFINITE);
        printf("Timer was signaled \n");
        MessageBeep(MB_ICONEXCLAMATION);
    }
 
    return 0;
}
cs



중간에 타이머를 해제하고자 하는 경우에 CancelWaitableTimer()를 이용하면 된다. 단 타이머를 소멸시키거나 할당된 자원을 반환하는 함수가 아니다.

소멸을 하고자 할 때는 CloseHandle()을 이용해야 한다.

'High Level Technique > Window System' 카테고리의 다른 글

캐쉬와 가상메모리  (0) 2017.01.05
쓰레드 풀 (Thread Pool)  (0) 2017.01.04
쓰레드 동기화  (0) 2017.01.03
Thread 생성과 소멸  (0) 2017.01.02
쓰레드 (Thread)  (0) 2017.01.02