https://dlemrcnd.tistory.com/58?category=528341
동기화 문제
스레드 A와 스레드 B로 왔다 갔다(스위칭)하면서 경쟁하는데
둘의 조건이 안맞아 스레드끼리 서로 대기하는 교착상태(DeadLock)이 발생한다.
교착상태 : 두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에
아무것도 완료되지 못하는 상태를 의미함,
위키피디아에서는 예시로 사다리에서 한 사람은 올라가려고 하고
한 사람은 내려갈라고 하는데, 서로 비켜줄 때까지 기다리는 걸로
예시를 들었다.
실행 순서를 제어해야 하는데, 이런 기술들을 동기화(Synchronization)라고 한다.
동기화 기법의 분류
유저 모드는 사용자, 즉 프로그래머의 처리이다. 외부에서 관여하지 못한다.
장점으로는 프로그래밍하기 쉽고, 속도가 빠르다. 단점으로는 제한된 기능을 갖는다.
커널 모드는 프로세스 관리하는 운영체제가 관여해 외부 프로그램이 접근 가능하다.
장점으로는 Deadlock 문제를 좀 더 명확히 막고, 둘 이상의 프로세스 간의 존재하는
스레드 간의 동기화도 가능하다. 운영체제가 관여하기 때문이다.
단점으로는 실행 속도의 저하가 발생한다
분류 | 종류 |
유저 모드 | 크리티컬 섹션, 인터락 함수 |
커널 모드 | 뮤텍스(Mutex), 세마포어(Semaphore), 이벤트 (Event) |
유저 모드 - 크리티컬 섹션
내부적으로 인터락 함수를 갖고 있다고 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
void InitializeCriticalSection(
[out] LPCRITICAL_SECTION lpCriticalSection
);
// lpCriticalSection : 초기화할 크리티컬 섹션 변수의 포인터를 전달
// 크리티컬 섹션 초기화
void EnterCriticalSection(
[out] LPCRITICAL_SECTION lpCriticalSection
);
// 임계영역에 들어간다. 다른 스레드는 동일 리소스 접근 못함
void LeaveCriticalSection(
[out] LPCRITICAL_SECTION lpCriticalSection
);
// 임계영역에서 떠남 . leave하지 않으면 영원히 데드락임
// 대기중인 스레드가 작업이 안끝났구나 해서 무한 대기함
void DeleteCriticalSection(
[out] LPCRITICAL_SECTION lpCriticalSection
);
// 크리티컬 섹션 해제,
|
cs |
Enter~ Leave로 감싸면 크리티컬 한 보호 상태가 된다.
유저 모드 - 인터락(Interlocked)
멀티스레드에서 안전하게 변숫값을 조작 하는 함수를 인터락이라한다.
예를 들어 ++-- 증감 연산자는 개발자 입장에서 한 줄이지만,
기계가 해석하는 어셈블리어에서는 한 줄이 아니다. 그 사이에서 스위칭이 발생함.
그래서 별도의 증가함수가 필요하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
LONG InterlockedIncrement(
LONG volatile *Addend
);
LONG InterlockedDecrement(
LONG volatile *Addend
);
LONG InterlockedAdd(
LONG volatile *Addend,
LONG Value
);
LONG InterlockedExchange(
LONG volatile *Target,
LONG Value
);
LONG InterlockedCompareExchange(
LONG volatile *Destination,
LONG ExChange,
LONG Comperand
);
|
cs |
커널 모드 - 뮤텍스 (Mutex)
뮤 텍스는 Mutual Exclusion의 줄임말. 상호 배제라는 뜻이다.
상호 배제는 두 스레드가 동시에 소유할 수 없다는 것을 의미한다.
뮤텍스는 신호상태(signaled)와 비신호상태(non-signaled)를 띤다.
신호상태는 스레드의 실행을 허가하고 비신호는 허가하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
//뮤텍스 생성함수
HANDLE CreateMutex(
[in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes, // 보안 관련 특성 전달 (NULL)
[in] BOOL bInitialOwner, // 초기 소유된 스레드 (바로 Signaled)
[in, optional] LPCSTR lpName // 객체 이름
);
//뮤텍스 반환 함수 Mutex 상태를 non-signaled -> signaled 로 바꿈
BOOL ReleaseMutex(
[in] HANDLE hMutex
);
//뮤텍스 소멸 함수
BOOL CloseHandle(
[in] HANDLE hObject
);
|
cs |
대기 함수
스레드의 실행을 블록 하여 대기시킨다.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 하나의 동기화 객체가 신호상태가 되기를 기다린다.
DWORD WaitForSingleObject(
[in] HANDLE hHandle,
[in] DWORD dwMilliseconds // INFINITE로 하면 무한정 기다림
);
//복수의 동기화 객체가 신호상태가 되길 기다림
DWORD WaitForMultipleObjects(
[in] DWORD nCount,
[in] const HANDLE *lpHandles,
[in] BOOL bWaitAll,
[in] DWORD dwMilliseconds
);
|
cs |
뮤텍스 예시
서버에서 유저 리스트를 업데이트할 때, 다른 스레드에서 대기 함수로 대기하는 경우이다.
뮤텍스 핸들을 선언한다.
두 개의 스레드에서 전역 변수를 안전하게 접근하기 위해 뮤텍스를 사용한다.
고로 , 스레드를 생성한다.
아래는 스레드 시작 함수들의 내용이다.
대기 함수로 뮤텍스 핸들을 넣어 ReleaseMutex가 일어날 때까지
다른 스레드는 대기한다. 그러면, 각 스레드는 컨텍스트 스위칭을
정신없이 하는 와중에도 유저 리스트(전역 변수)에 접근할 때는
unsignaled이 돼서, 같은 변수를 접근하려는 다른 스레드는 대기를 한다.
Sample.cpp
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
|
#include "Sample.h"
//서버
CRITICAL_SECTION g_CS;
HANDLE g_hMutex;
std::list<KNetworkUser> g_UserList;
KNetwork g_Net;
int BroadCast(KNetworkUser& user)
{
if (user.m_lPacketPool.size() > 0)
{
std::list<KPacket>::iterator iter;
for (iter = user.m_lPacketPool.begin();
iter != user.m_lPacketPool.end();)
{
for (KNetworkUser& senduser : g_UserList)
{
int iRet = g_Net.SendMsg(senduser.m_Sock, (*iter).m_uPacket);
if (iRet <= 0)
{
senduser.m_bConnect = false;
}
}
iter = user.m_lPacketPool.erase(iter);
}
}
return 1;
}
DWORD WINAPI RecvThread(LPVOID param)
{
SOCKET sock = (SOCKET)param;
while (1)
{
//스레드가 특정 시그널이 발생할때까지 기다림
WaitForSingleObject(g_hMutex, INFINITE);
std::list<KNetworkUser>::iterator iter;
for (iter = g_UserList.begin();
iter != g_UserList.end();)
{
int iRet = g_Net.RecvUser(*iter);
//0보다 작거나 같음, 받아온 데이터가 없거나, 에러일 경우
if (iRet <= 0)
{
iter = g_UserList.erase(iter);
}
else
{
iter++;
}
}
//운영체제한테 제어권 넘겨줌
ReleaseMutex(g_hMutex);
//의도적으로 Context Switching
Sleep(1);
}
}
DWORD WINAPI SendThread(LPVOID param)
{
SOCKET sock = (SOCKET)param;
while (1)
{
//스레드가 특정 시그널이 발생할때까지 기다림
WaitForSingleObject(g_hMutex, INFINITE);
std::list<KNetworkUser>::iterator iter;
for (iter = g_UserList.begin();
iter != g_UserList.end();)
{
//전체에게 보내줌
int iRet = BroadCast(*iter);
//0보다 작거나 같음, 받아온 데이터가 없거나, 에러일 경우
if (iRet <= 0)
{
iter = g_UserList.erase(iter);
}
else
{
iter++;
}
}
//운영체제한테 제어권 넘겨줌
ReleaseMutex(g_hMutex);
//의도적으로 Context Switching
Sleep(1);
}
}
void main()
{
//유저모드 동기화 모드 크리티컬섹션
//InitializeCriticalSection(&g_CS);
g_hMutex = CreateMutex(NULL, FALSE, NULL);
g_Net.InitNetwork();
g_Net.InitServer(SOCK_STREAM,10000, nullptr);
SOCKADDR_IN clientAddr;
int iLen = sizeof(clientAddr);
std::cout<< "Server Start." << std::endl;
//non blocking socket 0이면 블락킹 소켓
u_long on = 1;
ioctlsocket(g_Net.m_Sock, FIONBIO, &on);
DWORD ThreadID_Recv;
//데이터 받는 스레드
HANDLE hThreadRecv = ::CreateThread(
0,
0,
RecvThread, // 시작함수를 지정
(LPVOID)g_Net.m_Sock, // 시작함수 인자값
0, // 바로 시작할것인지 플래그
&ThreadID_Recv // 스레드 아이디 반환
);
CloseHandle(hThreadRecv);
DWORD ThreadID_Send;
HANDLE hThreadSend = ::CreateThread(
0,
0,
SendThread,
(LPVOID)g_Net.m_Sock,
0,
&ThreadID_Send
);
CloseHandle(hThreadSend);
//메인 스레드
while (1)
{
SOCKET clientSock = accept(g_Net.m_Sock,
(sockaddr*)&clientAddr, &iLen);
if (clientSock == SOCKET_ERROR)
{
int iError = WSAGetLastError();
if (iError != WSAEWOULDBLOCK)
{
std::cout << "ErrorCode=" << iError << std::endl;
break;
}
}
//클라이언트가 접속 시 시작함
else
{
KNetworkUser user;
user.set(clientSock, clientAddr);
//메인 스레드가 특정 시그널이 발생할때까지 기다림
WaitForSingleObject(g_hMutex, INFINITE);
//들어온 유저 전역 리스트에 추가
g_UserList.push_back(user);
ReleaseMutex(g_hMutex);
std::cout
<< "ip =" << inet_ntoa(clientAddr.sin_addr)
<< "port =" << ntohs(clientAddr.sin_port)
<< " " << std::endl;
u_long on = 1;
ioctlsocket(clientSock, FIONBIO, &on);
std::cout << std::to_string(g_UserList.size())<< " 명 접속중." << std::endl;
}
Sleep(1);
}
closesocket(g_Net.m_Sock);
WSACleanup();
CloseHandle(g_hMutex);
}
|
cs |
결과
세마포어(Semaphore)
세마포어는 뮤텍스와 유사하지만 카운팅이 가능한 함수로
세마포어가 0이 되어야 non-signaled이 된다.
예를 들어 크롬에서 다운로드를 할 때 3개를 동시에 다운로드 중이라면
4번째부터는 대기 상태에 들어가는 메커니즘과 유사하다.
이벤트 (Event)
어떤 사건이 일어났음을 알리는 동기화 객체이다.
윈도우의 메시지와 유사한 기능으로 자동리셋 이벤트와 수동 리셋 이벤트가 있다.
자동은 대기하던 스레드가 해제되면 자동으로 리셋이 되고, 수동은 자동으로 리셋이 안된다.
프로그래밍 적으로 해줘야 함.
'STUDY > Network Programming' 카테고리의 다른 글
Network Programming - IOCP 완료 포트 (I/O completion port ) (0) | 2022.01.19 |
---|---|
Network Programming - 비동기 파일 입출력이 가능한 WinAPI 파일 입출력 (Overlapped 입출력) (1) | 2022.01.14 |
Network Programming - 윈도우 메세지 통해 소켓 입출력, WSAAsyncSelect 모델 (11) | 2022.01.10 |
Network Programming - 다중 입출력 함수 Select() (0) | 2022.01.07 |
Network Programming - 소켓모드의 Blocking & NonBlocking (0) | 2022.01.06 |