2023.11.19 - [Network/소켓통신(Linux)] - 멀티스레드: 리눅스 1
2. 멀티스레드 TCP 서버
1) 멀티스레드 TCP 서버 동작
- 클라이언트가 접속하면 accept() 함수는 클라이언트와 통신할 수 있는 소켓 리턴
- 클라이언트와 통신을 담당할 스레드를 생성
이때, 스레드 함수에 소켓을 넘겨줌 - 스레드 함수는 인수로 전달된 소켓을 소켓 타입(정수형)으로 변환하여 저장
- getpeername() 함수를 호출하여 클라이언트의 IP 주소와 포트 번호를 얻음
이 코드는 필수는 아니며 클라이언트 정보 출력 시에만 사용 - 클라이언트와 데이터를 주고받음
EX) 멀티스레드 TCP 서버 작성과 테스트
- 클라이언트는 기존의 코드 사용
2023.09.17 - [Network/소켓통신(Linux)] - 데이터 전송하기
#include "../Common.h"
#define SERVERPORT 9000
#define BUFSIZE 512
// 클라이언트와 데이터 통신
void *ProcessClient(void *arg)
{
int retval;
SOCKET client_sock = (SOCKET)(long long)arg;
struct sockaddr_in clientaddr;
char addr[INET_ADDRSTRLEN];
socklen_t addrlen;
char buf[BUFSIZE + 1];
// 클라이언트 정보 얻기
addrlen = sizeof(clientaddr);
getpeername(client_sock, (struct sockaddr *)&clientaddr, &addrlen);
inet_ntop(AF_INET, &clientaddr.sin_addr, addr, sizeof(addr));
while (1) {
// 데이터 받기
retval = recv(client_sock, buf, BUFSIZE, 0);
if (retval == SOCKET_ERROR) {
err_display("recv()");
break;
}
else if (retval == 0)
break;
// 받은 데이터 출력
buf[retval] = '\0';
printf("[TCP/%s:%d] %s\n", addr, ntohs(clientaddr.sin_port), buf);
// 데이터 보내기
retval = send(client_sock, buf, retval, 0);
if (retval == SOCKET_ERROR) {
err_display("send()");
break;
}
}
// 소켓 닫기
close(client_sock);
printf("[TCP 서버] 클라이언트 종료: IP 주소=%s, 포트 번호=%d\n",
addr, ntohs(clientaddr.sin_port));
return 0;
}
int main(int argc, char *argv[])
{
int retval;
// 소켓 생성
SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock == INVALID_SOCKET) err_quit("socket()");
// bind()
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()");
// listen()
retval = listen(listen_sock, SOMAXCONN);
if (retval == SOCKET_ERROR) err_quit("listen()");
// 데이터 통신에 사용할 변수
SOCKET client_sock;
struct sockaddr_in clientaddr;
socklen_t addrlen;
pthread_t tid;
while (1) {
// accept()
addrlen = sizeof(clientaddr);
client_sock = accept(listen_sock, (struct sockaddr *)&clientaddr, &addrlen);
if (client_sock == INVALID_SOCKET) {
err_display("accept()");
break;
}
// 접속한 클라이언트 정보 출력
char addr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &clientaddr.sin_addr, addr, sizeof(addr));
printf("\n[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n",
addr, ntohs(clientaddr.sin_port));
// 스레드 생성
retval = pthread_create(&tid, NULL, ProcessClient,
(void *)(long long)client_sock);
if (retval != 0) { close(client_sock); }
}
// 소켓 닫기
close(listen_sock);
return 0;
}
- 두 개 이상의 클라이언트가 접속해도 독립적으로 처리가 진행
3. 스레드 동기화(Thread Synchronization)
1) 스레드 동기화 필요성
- 멀티스레드를 이용하는 프로그램에서 두 개 이상의 스레드가 공유 데이터에 접근하면 문제 발생
종류 | 기능 |
뮤텍스(Mutex) | 공유 자원에 오직 한 스레드의 접근만 허용 |
읽기-쓰기 잠금(Reader-Writer Lock) | 공유 자원에 대한 읽기는 여러 스레드에 허용 공유 자원에 대한 쓰기는 한 스레드만 허용 |
조건 변수(Condition Variable) | 특정 조건이 만족되면 대기 중인 스레드를 깨움 |
배리어(Barrier) | 일정 개수의 스레드가 특정 코드에 도달하기 전까지 모두 대기 상태 특정 코드에 도달하면 대기 중인 모든 스레드를 깨움 |
2) 스레드 동기화 기본 개념
- 스레드 동기화가 필요한 상황
- 둘 이상의 스레드가 공유 자원에 접근
- 한 스레드가 작업을 완료한 후, 기다리는 다른 스레드에 알려줌
- 스레드를 동기화 하려면 스레드가 상호 작용 해야하므로 중간 매개체가 필요
- 두 스레드는 매개체를 통해 진행 가능 여부를 판단해 자신의 실행을 계속할 지 결정
3) 뮤텍스(Mutex: Mutual Exclusion)
- 두 개 이상의 스레드가 공유 자원에 접근할 때, 오직 한 스레드만 접근을 허용해야 하는 상황에서 사용
- 상호 배제의 원리
EX) 뮤텍스 연습
include <stdio.h>
#include <pthread.h>
#define MAXCNT 100000000
int count = 0;
pthread_mutex_t mutex;
void *MyThread1(void *arg)
{
for (int i = 0; i < MAXCNT; i++) {
pthread_mutex_lock(&mutex);
count += 2;
pthread_mutex_unlock(&mutex);
}
return 0;
}
void *MyThread2(void *arg)
{
for (int i = 0; i < MAXCNT; i++) {
pthread_mutex_lock(&mutex);
count -= 2;
pthread_mutex_unlock(&mutex);
}
return 0;
}
int main(int argc, char *argv[])
{
// 뮤텍스 초기화
pthread_mutex_init(&mutex, NULL);
// 스레드 두 개 생성
pthread_t tid[2];
pthread_create(&tid[0], NULL, MyThread1, NULL);
pthread_create(&tid[1], NULL, MyThread2, NULL);
// 스레드 두 개 종료 대기
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
// 뮤텍스 삭제
pthread_mutex_destroy(&mutex);
// 결과 출력
printf("count = %d\n", count);
return 0;
}
- 올바른 값인 0 출력
- 스레드 동기화로 인한 오버헤드가 발생해 결과 출력까지 시간이 걸림
- 극단적인 상황에서 지나치게 세밀한 단위로 스레드 동기화를 하고 있어 성능저하가 두드러지는 예제
4) 조건 변수(Condition Variable)
- 어떤 조건을 검사하여 조건이 만족되면 다른 스레드에 알리는 동기화 기법
- 한 스레드가 작업을 완료한 후 기다리고 있는 다른 스레드에 알릴 때 사용 가능
- 다른 동기화 기법보다 사용법이 까다로움
- 뮤텍스로 묶어서 보호
EX) 조건 변수 연습
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#define BUFSIZE 10
pthread_cond_t writeCond;
pthread_cond_t readCond;
pthread_mutex_t writeMutex;
pthread_mutex_t readMutex;
int writeDone = 0;
int readDone = 0;
int buf[BUFSIZE];
void *WriteThread(void *arg)
{
for (int k = 1; k <= 500; k++) {
// 읽기 완료 대기
pthread_mutex_lock(&readMutex);
while (readDone == 0)
pthread_cond_wait(&readCond, &readMutex);
readDone = 0;
pthread_mutex_unlock(&readMutex);
// 공유 버퍼에 데이터 저장
for (int i = 0; i < BUFSIZE; i++)
buf[i] = k;
// 쓰기 완료 알림
pthread_mutex_lock(&writeMutex);
writeDone = 1;
pthread_mutex_unlock(&writeMutex);
pthread_cond_signal(&writeCond);
}
return 0;
}
void *ReadThread(void *arg)
{
while (1) {
// 쓰기 완료 대기
pthread_mutex_lock(&writeMutex);
while (writeDone == 0)
pthread_cond_wait(&writeCond, &writeMutex);
writeDone = 0;
pthread_mutex_unlock(&writeMutex);
// 읽은 데이터 출력 후 버퍼를 0으로 초기화
printf("Thread %4d:\t", (int)pthread_self());
for (int i = 0; i < BUFSIZE; i++)
printf("%3d ", buf[i]);
printf("\n");
memset(buf, 0, sizeof(buf));
// 읽기 완료 알림
pthread_mutex_lock(&readMutex);
readDone = 1;
pthread_mutex_unlock(&readMutex);
pthread_cond_signal(&readCond);
}
return 0;
}
int main(int argc, char *argv[])
{
// 조건 변수와 뮤텍스 생성
pthread_cond_init(&writeCond, NULL);
pthread_cond_init(&readCond, NULL);
pthread_mutex_init(&writeMutex, NULL);
pthread_mutex_init(&readMutex, NULL);
// 스레드 세 개 생성
pthread_t tid[3];
pthread_create(&tid[0], NULL, WriteThread, NULL);
pthread_create(&tid[1], NULL, ReadThread, NULL);
pthread_create(&tid[2], NULL, ReadThread, NULL);
// 읽기 완료 알림
pthread_mutex_lock(&readMutex);
readDone = 1;
pthread_mutex_unlock(&readMutex);
pthread_cond_signal(&readCond);
// 스레드 세 개 종료 대기
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
pthread_join(tid[2], NULL);
// 조건 변수와 뮤텍스 제거
pthread_cond_destroy(&writeCond);
pthread_cond_destroy(&readCond);
pthread_mutex_destroy(&writeMutex);
pthread_mutex_destroy(&readMutex);
return 0;
}
- 총 500번에 걸쳐 버퍼에 데이터를 저장할 때 마다 두 스레드 중 하나가 대기 상태에서 깨어나 버퍼 데이터를 읽고 출력
- 출력행마다 다른 스레드 ID를 확인
'Network > 소켓통신(Linux)' 카테고리의 다른 글
멀티스레드: 리눅스 1 (1) | 2023.11.19 |
---|---|
멀티스레드 개념 (2) | 2023.11.19 |
TCP 동작 과정 및 함수 정리 (0) | 2023.11.19 |
데이터 전송하기 (0) | 2023.09.17 |
TCP 서버 - 클라이언트 구조 (2) | 2023.09.02 |