Network/소켓통신(Linux)

멀티스레드 : 리눅스 2

busy맨 2023. 11. 19. 23:15

2023.11.19 - [Network/소켓통신(Linux)] - 멀티스레드: 리눅스 1

 

멀티스레드: 리눅스 1

2023.11.19 - [Network/소켓통신(Linux)] - 멀티스레드 개념 멀티스레드 개념 1. 스레드 기초 1) 소켓 응용 프로그램과 멀티스레드 멀티스레드를 사용하지 않는 경우 문제 두 개 이상의 클라이언트가 서버

kmg0157.tistory.com

 

2. 멀티스레드 TCP 서버

1)  멀티스레드 TCP 서버 동작

  1. 클라이언트가 접속하면 accept() 함수는 클라이언트와 통신할 수 있는 소켓 리턴
  2. 클라이언트와 통신을 담당할 스레드를 생성
    이때, 스레드 함수에 소켓을 넘겨줌
  3. 스레드 함수는 인수로 전달된 소켓을 소켓 타입(정수형)으로 변환하여 저장
  4. getpeername() 함수를 호출하여 클라이언트의 IP 주소와 포트 번호를 얻음
    이 코드는 필수는 아니며 클라이언트 정보 출력 시에만 사용
  5. 클라이언트와 데이터를 주고받음

EX) 멀티스레드 TCP 서버 작성과 테스트

  • 클라이언트는 기존의 코드 사용

2023.09.17 - [Network/소켓통신(Linux)] - 데이터 전송하기

 

데이터 전송하기

1. 응용 프로그램 프로토콜과 데이터 전송 1)응용 프로그램 프로토콜(Application Protocol) 응용 프로그램 수준에서 주고받는 데이터의 형식과 의미, 처리 방식을 정의한 프로토콜 표준화 되어 있지

kmg0157.tistory.com

#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;
}

서버에 클라이언트 2개 접속
두 클라이언트에서 각각 서버로 데이터 전달
서버에서 전달받은 데이터 출력

  • 두 개 이상의 클라이언트가 접속해도 독립적으로 처리가 진행

 

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