코딩/Network

[네트워크] 멀티스레드 TCP서버/스레드 동기화/임계영역/이벤트

나야, 웅이 2023. 6. 5. 11:51
728x90
반응형
SMALL

목차

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;
}
728x90
반응형
LIST