목차
1. 멀티스레드 TCP 서버
2. 스레드 동기화
3. 임계영역
4. 이벤트
멀티스레드 TCP 서버
기본형태
1. 클라이언트가 접속하면 accept() : accept()는 클라이언트와 통신할 소켓을 리턴
2. 클라이언트와 통신할 스레드 생성 CreateThread() : 이때 클라이언트와 통신할 소켓을 스레드에 인자로 넘겨줘야함
3. 스레드 함수는 인자로 전달된 소켓을 SOCKET 타입으로 저장
4. getpeername() 함수를 호출하여 클라이언트 주소 정보 획득
5. 클라이언트와 데이터 통신
#include "../../common.h"
#include <string.h>
#define SERVERPORT 9000
#define BUFSIZE 512
// thread
DWORD WINAPI ProcessClient(LPVOID arg)
{
// 3. 인자로 전달된 소켓을 SOCKET 타입으로 저장
SOCKET client_sock = (SOCKET)arg;
int retval = 0;
struct sockaddr_in clientaddr;
char addr[INET_ADDRSTRLEN] = { 0, };
int addrlen = 0;
char buf[BUFSIZE + 1] = { 0, };
// 4. getpeername()으로 클라이언트 주소 정보를 획득
addrlen = sizeof(clientaddr);
getpeername(client_sock, (struct sockaddr*)&clientaddr, &addrlen);
inet_ntop(AF_INET, &clientaddr.sin_addr, addr, sizeof(addr));
// 5. 클라이언트와 데이터 통신
while (1)
{
// recv
retval = recv(client_sock, buf, BUFSIZE, 0);
if (SOCKET_ERROR == retval)
{
err_display("recv()");
break;
}
else if (0 == retval)
break;
buf[retval] = '\0';
printf("[TCP/%s:%d] %s\n", addr, ntohs(clientaddr.sin_port), buf);
// send
retval = send(client_sock, buf, retval, 0);
if (SOCKET_ERROR == retval)
{
err_display("send()");
break;
}
}
closesocket(client_sock);
printf("[TCP server] client shutdown: IP=%s, Port=%d\n", addr, ntohs(clientaddr.sin_port));
return 0;
}
int main(int argc, char* argv[])
{
int retval = -1;
// initialize winsock
WSADATA wsa;
if (0 != WSAStartup(MAKEWORD(2, 2), &wsa))
return 1;
printf("[TCP Server] WSAStartup Success.\n");
// create socket
SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == listen_sock)
err_quit("socket()");
// bind socket
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(SERVERPORT);
retval = bind(listen_sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if (retval == SOCKET_ERROR)
err_quit("bind()");
printf("[TCP Server] bind() Success.\n");
// listen
retval = listen(listen_sock, SOMAXCONN);
if (SOCKET_ERROR == retval)
err_quit("listen()");
printf("[TCP Server] listen() Success.\n");
// data communication
SOCKET client_sock;
struct sockaddr_in clientaddr;
int addrlen;
HANDLE hThread = NULL;
// char buf[BUFSIZE + 1];
while (1)
{
// 1. accept()
addrlen = sizeof(clientaddr);
client_sock = accept(listen_sock, (struct sockaddr*)&clientaddr, &addrlen);
if (INVALID_SOCKET == client_sock)
{
err_display("accept()");
break;
}
// print the client information
char addr[INET_ADDRSTRLEN] = { 0, };
inet_ntop(AF_INET, &clientaddr.sin_addr, addr, sizeof(addr));
printf("\n[TCP Server] Client IP = %s, port = %d\n", addr, ntohs(clientaddr.sin_port));
// 2. CreateThread()
hThread = CreateThread(NULL, 0, ProcessClient, (LPVOID)client_sock, 0, NULL);
if (NULL == hThread)
closesocket(client_sock);
else
CloseHandle(hThread);
}
// close socket
closesocket(listen_sock);
// close winsock
WSACleanup();
return 0;
}
getpeername() / getsockname()
int getpeername(
[in] SOCKET s,
[out] sockaddr *name,
[in, out] int *namelen
);
[in] s
연결된 소켓을 식별하는 설명자
[out] name
피어의 주소를 받는 SOCKADDR 구조체
[in, out] namelen
이름 매개 변수의 크기(바이트)에 대한 포인터
int getsockname(
[in] SOCKET s,
[out] sockaddr *name,
[in, out] int *namelen
);
[in] s
소켓을 식별하는 설명자
[out] name
소켓의 주소(이름)을 받는 SOCKADDR 구조체에 대한 포인터
[in, out] namelen
이름 버퍼의 크기(바이트)
스레드 동기화
스레드 동기화 문제
스레드 동기화 문제
여러 스레드가 동시에 실행 될 때, 2개 이상의 스레드가 동일한 자원에 접근할 때 문제가 발생할 수 있다.
그러므로, 하나의 스레드가 공유자원에 접근하여 조작하고 있는 경우 다른 스레드의 접근을 못하도록 해야한다.
예를들어, money를 마지막에 출력했을 때 7000이 되도록 하려고한다.
스레드1, 스레드2가 실행되면
스레드1의 ① ecx = 1000 ② ecx = 3000 ③ money = 3000
스레드2의 ① ecx = 1000 ② ecx = 5000 ③ money = 5000
으로 원하는 값이 안나온다.
그래서 한 스레드가 먼저 공유자원 money에 접근하여 조작할 때 다른 스레드가 접근하여 조작하지 못하도록 해야한다.
공유자원에 대한 접근을 제어하는 것이 스레드 동기화이다.
스레드 동기화 처리
스레드 동기화 기법
임계영역
- Create_() 사용 X = 커널영역이 아님
-한 프로세스에 속한 스레드 간에만 사용 가능
뮤텍스, 이벤트. 세마포어, 대기 가능타이머
- Create_()를 사용함 = 커널메모리 영역에 동기화 객체
- 서로다른 프로세스에 속한 스레드간에도 사용 가능
스레드 동기화가 필요한 상황
1. 둘 이상의 스레드가 공유 자원에 접근
2. 한 스레드가 작업을 완료한 후, 기다리고 있는 다른 스레드에 알려줌
스레드 동기화 원리
스레드 1, 2의 상태를 매개체를 통해 상태 정보를 전달
동기화 객체의 특징
Create_() 함수를 호출하면 커널 메모리 영역에 동기화 객체가 생성되고
이에 접근할 수 있는 핸들이 리턴
평소에는 비신호 상태로 있다가, 특정 조건이 만족되면 신호 상태가 됨
비신호 상태에서 신호 상태로 변화 여부는 Wait_() 함수를 사용해 감지
사용이 끝나면, CloseHandle() 호출
임계 영역
두 개 이상의 스레드가 공유 자원에 접근할 때,
오직 한 스레드만 접근을 허용해야 하는 경우에 사용
임계 영역 특징
- 프로세스의 사용자 메모리 영역에 존재하는 단순한 구조체이므로, 한 프로세스에 속한 스레드 간 동기화에만 사용- 다른 동기화 객체보다 빠르고 효율적(커널에 있는 것이 아니라, 프로세스 내부에 있어서)
#include <windows.h>
// CRITICAL_SECTION 구조체 타입의 전역 변수를 선언
// 일반동기화 객체는 Create_() 함수를 호출하여 커널 메모리 영역에 생성하지만
// 임계영역은 사용자 메모리 영역에 (주로 전역변수 형태로) 생성한다.
CRITICAL_SECTION cs;
// 스레드
DWORD WINAPI MyThread1(LPVOID arg)
{
...
EnterCriticalSection(&cs);
// 공유자원에 접근하기 전에 호출
// 공유자원을 사용하고 있는 스레드가 없다면 곧바로 리턴
// 공유자원을 사용중인 스레드가 있으면 리턴하지 못하고 호출 스레드는 곧바로 대기상태가 됨
// 공유 자원 접근
LeaveCriticalSection(&cs);
// 공유자원 사용을 마치면 호출
// 이때 EnterCriticalSection()함수에서 대기중인 다른 스레드가 있다면 하나만 선택되어 깨어난다
...
}
int main(int argc, char* argv[])
{
...
InitializeCriticalSection(&cs); // 임계영역을 사용하기 전에 호출하여 초기화
// 스레드를 둘 이상 생성하여 작업을 진행한다.
// 생성한 모든 스레드가 종료할 때까지 기다린다.
DeleteCriticalSection(&cs); // 임계영역을 사용하는 모든 스레드가 종료하면 호출하여 삭제한다.
...
}
임계영역 CRITICAL_SECTION 사용한 코드
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <mutex>
#define ITER 1000000
#ifndef MY_CS // 만약 MY_CS가 define되어 있지 않으면 MY_CS를 define해라
#define MY_CS
#endif
int num = 0;
#ifdef MY_CS
CRITICAL_SECTION cs;
#endif
DWORD WINAPI add_thread(LPVOID arg)
{
for (int i = 0; i < ITER; i++)
{
#ifdef MY_CS
EnterCriticalSection(&cs);
#endif
num += 2;
#ifdef MY_CS
LeaveCriticalSection(&cs);
#endif
}
return 0;
}
DWORD WINAPI substract_thread(LPVOID arg)
{
for (int i = 0; i < ITER; i++)
{
#ifdef MY_CS
EnterCriticalSection(&cs);
#endif
num -= 2;
#ifdef MY_CS
LeaveCriticalSection(&cs);
#endif
}
return 0;
}
int main(int argc, char* argv[])
{
printf("before: num = %d\n", num);
#ifdef MY_CS
InitializeCriticalSection(&cs);
#endif
HANDLE hThread[2];
hThread[0] = CreateThread(NULL, 0, add_thread, NULL, CREATE_SUSPENDED, NULL);
hThread[1] = CreateThread(NULL, 0, substract_thread, NULL, CREATE_SUSPENDED, NULL);
ResumeThread(hThread[0]);
ResumeThread(hThread[1]);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
#ifdef MY_CS
DeleteCriticalSection(&cs);
#endif
printf("after: num = %d\n", num);
return 0;
}
임계영역을 사용하지 않은 코드
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <mutex>
#define ITER 1000000
#ifndef MY_CS
//#define MY_CS //CRITICAL_SECTION 주석처리
#endif
int num = 0;
#ifdef MY_CS
CRITICAL_SECTION cs;
#endif
DWORD WINAPI add_thread(LPVOID arg)
{
for (int i = 0; i < ITER; i++)
{
#ifdef MY_CS
EnterCriticalSection(&cs);
#endif
num += 2;
#ifdef MY_CS
LeaveCriticalSection(&cs);
#endif
}
return 0;
}
DWORD WINAPI substract_thread(LPVOID arg)
{
for (int i = 0; i < ITER; i++)
{
#ifdef MY_CS
EnterCriticalSection(&cs);
#endif
num -= 2;
#ifdef MY_CS
LeaveCriticalSection(&cs);
#endif
}
return 0;
}
int main(int argc, char* argv[])
{
printf("before: num = %d\n", num);
#ifdef MY_CS
InitializeCriticalSection(&cs);
#endif
HANDLE hThread[2];
hThread[0] = CreateThread(NULL, 0, add_thread, NULL, CREATE_SUSPENDED, NULL);
hThread[1] = CreateThread(NULL, 0, substract_thread, NULL, CREATE_SUSPENDED, NULL);
ResumeThread(hThread[0]);
ResumeThread(hThread[1]);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
#ifdef MY_CS
DeleteCriticalSection(&cs);
#endif
printf("after: num = %d\n", num);
return 0;
}
임계 영역 사용 시 주의사항
임계 영역만으로는 어느 스레드가 먼저 리소스를 사용할지 결정할 수 없음.
어느 스레드가 먼저 EnterCriticalSection() 함수를 호출할 지 알 수 없음
스레드 1이 동작 중일 때, 스레드2와 스레드3이 EnterCriticalSection()하려고 준비 중이다.
이때 스레드1이 LeaveCriticalSection()하면 스레드2와 스레드3 중에 어떤 스레드가 EnterCriticalSection()할 지 모르는 문제가 생김
=> 이를 해결하기 위해 이벤트를 세팅함
이벤트
사건 발생을 다른 스레드에 알리는 동기화 기법
이벤트를 사용하는 전형적인 절차
1. 이벤트를 비신호(Non-signaled state) 상태로 생성
2 한 스레드가 작업을 진행하고, 나머지 스레드는 이벤트에 대해 wait*()함수를 호출해 이벤트가 신호(signaled state)상태가 될 때까지 대기
3. 스레드가 작업을 완료하면 이벤트를 신호 상태로 변경
4. 대기 중인 스레드 중 한 개 혹은 전부가 깨어남. 깨어난 스레드는 후속 작업을 함
5. 이벤트가 필요하지 않으면 CloseHandle() 함수를 호해 이벤트를 제거
이벤트 상태 변경
BOOL SetEvent(HANDLE hEvent); // 비신호 상태 -> 신호상태
BOOL ResetEvent(HANDLE hEvent); // 신호 상태 -> 비신호 상태
HANDLE CreateEventA(
[in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes,
[in] BOOL bManualReset,
[in] BOOL bInitialState,
[in, optional] LPCSTR lpName
);
[in] bManualReset
TRUE면 수동 리셋, FALSE면 자동 리셋
[in] bInitialState
TRUE면 신호 상태, FALSE면 비신호 상태
[in, optional] lpName
Event 이름
-이름이 없으면 같은 프로세스 내에서만 사용 가능
-이름을 만들면 서로 다른 프로세스에서도 사용 가능
=> 모두 CreateEvent하지만 처음 생성하는 쪽은 생성, 다른 쪽은 Open을 하게 됨
이벤트의 종류
자동 리셋 이벤트
- 이벤트를 신호 상태로 바꾸면, 대기 중인 스레드 중 하나만 깨운 후 자동으로 비신호 상태가 됨
수동 리셋 이벤트
- 이벤트를 신호 상태로 바꾸면, 대기 중인 스레드를 모두 깨운 후 계속 신호 상태를 유지
이벤트 실습
#include <windows.h>
#include <stdio.h>
#define BUFSIZE 10
HANDLE hWriteEvent = NULL;
HANDLE hReadEvent = NULL;
int buf[BUFSIZE] = { 0, };
DWORD WINAPI WriteThread(LPVOID arg)
{
DWORD retval;
for (int k = 0; k < 500; k++)
{
// 읽기 완료 대기
retval = WaitForSingleObject(hReadEvent, INFINITE);
if (retval != WAIT_OBJECT_0) break;
// 공유 버퍼에 데이터 저장
for (int i = 0; i < BUFSIZE; i++)
buf[i] = k;
// 쓰기 완료 알림
SetEvent(hWriteEvent);
}
return 0;
}
DWORD WINAPI ReadThread(LPVOID arg)
{
DWORD retval;
while (1) {
// 쓰기 완료 대기
retval = WaitForSingleObject(hWriteEvent, INFINITE);
if (retval != WAIT_OBJECT_0) break;
// 읽은 데이터 출력 후 버퍼를 0으로 초기화
printf("Thread %4d:\t", GetCurrentThreadId());
for (int i = 0; i < BUFSIZE; i++)
printf("%3d ", buf[i]);
printf("\n");
memset(buf, 0, sizeof(buf));
// 읽기 완료 알림
SetEvent(hReadEvent);
}
return 0;
}
int main(int argc, char *argv[])
{
// 이벤트 생성
hWriteEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
hReadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
// 스레드 세 개 생성
HANDLE hThread[3];
hThread[0] = CreateThread(NULL, 0, WriteThread, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, ReadThread, NULL, 0, NULL);
hThread[2] = CreateThread(NULL, 0, ReadThread, NULL, 0, NULL);
// 읽기 완료 알림
SetEvent(hReadEvent);
// 스레드 세 개 종료 대기
WaitForMultipleObjects(3, hThread, TRUE, INFINITE);
// 이벤트 제거
CloseHandle(hWriteEvent);
CloseHandle(hReadEvent);
return 0;
}
'코딩 > Network' 카테고리의 다른 글
[네트워크] 브로드캐스팅 (0) | 2023.06.06 |
---|---|
[네트워크] UDP 서버-클라이언트 (0) | 2023.06.05 |
[네트워크] 스레드 API, 스레드 제어 (0) | 2023.04.26 |
[네트워크] 멀티스레드 (0) | 2023.04.15 |
[네트워크] 데이터 전송, 고정 길이, 가변 길이 (0) | 2023.04.15 |