-
TCP/IP 소켓 프로그래밍 - 18장 : 멀티쓰레드 기반의 서버구현네트워크 2024. 6. 29. 02:06
18장18-1 쓰레드의 이론적 이해
멀티프로세스 방식의 서버는 다음과 같은 단점을 가진다.
- 프로세스 생성은 운영체제 차원에서 비용이 큰 방법이다.
- 프로세스마다 독립된 메모리 공간을 가지기 때문에 별도의 IPC 기법이 필요하다.
- 컨텍스트 스위칭에 따른 비용이 가장 큰 문제이다.
쓰레드는 멀티프로세스의 특징을 유지하면서도 단점을 줄이고자 등장하였다.
( 멀티 프로세스 방식은 한 코어 내에서 cpu 할당시간을 나누어,
사용자에게 동시에 돌아가는 듯한 서비스를 제공한다. )
쓰레드의 장점은 다음과 같다.
- 쓰레드 생성 및 컨텍스트 스위칭은 프로세스의 생성 및 컨텍스트 스위칭 보다 빠르다.
- 쓰레드끼리의 데이터 교환은 IPC 같은 별도의 방법이 필요없다.
프로세스는 아래와 같은 메모리 구조를 보인다.

보다시피 메모리 영역이 분리되어 있다.
data 영역은 전역변수가 할당되고,
heap영역은 동적으로 할당되는 영역,
stack 영역은 함수의 실행 등이 이 영역에서 다뤄진다.
만약 프로그래머가 하고 싶은 일이 둘 이상의 실행흐름을 가지는 것이라면
메모리 구조를 완전히 분리시킬 필요 없이, 스택 영역만 분리시키면 된다.
그렇게 함으로써 얻을 수 있는 장점은 다음과 같다.
- 컨텍스트 스위칭 시, 데이터와 heap 영역은 메모리에 올리고 내릴 필요가 없다.
- 같이 사용하는 data 영역과 heap 영역을 이용해 데이터를 교환할 수 있다.
쓰레드의 메모리 구조는 아래와 같다.

프로세스와 쓰레드를 다시 정의해보면 아래와 같다.
- 프로세스 : 운영체제 관점에서 실행흐름을 구성하는 단위
- 쓰레드 : 프로세스 관점에서 실행흐름을 구성하는 단위
( 운영체제를 공부해보면, 유저레벨 쓰레드, 커널레벨 쓰레드, 이를 혼합한 방법 등이있는데 기회되면 찾아보는 것도?)
18-2 쓰레드의 생성 및 실행
POSIX는 UNIX 계열 운영체제 간의 이식성을 높이기 위해 규정한 표준 API 규격이다.
쓰레드는 쓰레드만의 main 함수를 정의해주어야 한다.
이후, 이 함수를 시작으로 별도의 실행흐름 생성을 OS 에게 요청해야 한다.
그때 쓰이는 함수는 pthread_create이다.
#include <pthread.h> int pthread_create( pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg ); //성공 시 0, 실패시 0이외 값 리턴 //thread : 생성할 쓰레드 ID 저장할 주소 값 //attr : 쓰레드 특성 정보, NULL 전달시 기본특성 //start_routine : 쓰레드의 main함수 역할. 별도 실행흐름이 시작되는 함수 주소값 //arg : 쓰레드의 main함수가 호출될 때, 전달할 인자 정보 담은 변수 주소값sleep함수를 이용해 쓰레드의 실행흐름을 관리하는 것은 잘못된 방법이다!
프로그램의 흐름을 예측한다는 것은 불가능에 가깝다!
쓰레드의 실행흐름을 관리하는 방법은 pthread_join 함수를 사용하는 것이다!
#include <pthread.h> int pthread_join(pthread_t thread, void **status); //성공 시 0, 실패 시 0 이외의 값 리턴 //thread : 이 id를 가지는 쓰레드가 종료될 때까지 함수를 호출한 프로세스 또는 쓰레드가 // 대기상태에 놓인다. //status : thread의 main함수가 리턴한 값이 저장될 포인터 변수 주소값pthread_join함수는 원하는 쓰레드가 종료될 때까지 호출한 쓰레드/프로세스가 대기상태에 놓인다.
쓰레드의 main함수의 리턴 값도 확인할 수 있는 함수이다.
둘 이상의 쓰레드를 생성해서 이를 사용할 때 주의할 점이 있다.
바로 임계 영역이다.
임계 영역은 둘 이상의 쓰레드가 동시에 실행하면 문제를 일으키는 문장이 포함된 코드 블록이다.
임계영역 관련해 함수는 다음 두 가지로 구분된다.
- 쓰레드에 안전한 함수 ( Thread-safe func )
- 쓰레드에 불안전한 함수 ( Thread - unsafe func)
쓰레드에 안전한 함수도 임계영역이 존재할 수 있으나,
여러 쓰레드가 동시에 접근해도 문제를 일으키지 않도록 조치가 취해져 있다.
쓰레드에 불안전한 함수가 정의되어 있는 경우,
같은 기능을 갖는 쓰레드에 안전한 함수가 정의되어있다!!
헤더파일 선언 전에 매크로 _REENTRANT를 정의해보자!
( 컴파일 시 -D_REENTRANT로도 가능하다. )
gcc -D_REENTRANT mythread.c -o mthread -lpthread
18-3 쓰레드의 문제점과 임계영역
예제 (thread4.c )를 실행했을 때 생기는 문제점은,
전역변수에 둘 이상의 쓰레드가 동시에 접근했기 때문에 발생한 것이다.
꼭 전역변수가 아니더라도 메모리 공간을 동시에 접근하면 문제가 될 수 있다.
프로그래머가 기대하는 명령어 실행과 cpu가 명령어를 처리하는 과정은 살짝 괴리가 있다.
우리는 명령어 한 문장이 한 번에 실행되기를 기대하지만 ( a ++ ; )
cpu가 위 명령어를 처리할 때, load, add ,store 등의 단계를 거치게 된다.
위 작업이 다 처리된 이후에 해당 메모리 영역에 다른 쓰레드가 접근하는 건 상관 없지만,
위 작업 도중, 다른 쓰레드가 들어오게 되면 전혀 다른 결과를 낳게 되는 것이다.
정리하면, 한 쓰레드가 특정 메모리 공간에 접근해 연산을 완료할 때까지, 다른 쓰레드가 그 공간에 접근하지 못하도록 막아야 한다. 그리고 이를 “동기화”라고 부른다.
예제 thread4.c에서 임계영역은 다음과 같았다.
... num += 1; ... num -= 1; ...
18-4 쓰레드 동기화
쓰레드의 동기화는 쓰레드 접근 순서 때문에 생기는 문제의 해결책이다.
동기화가 필요한 상황은 아래와 같다.
- 동일 메모리 영역으로 동시접근 발생
- 동일 메모리 영역에 접근하는 쓰레드 실행 순서 지정
( OS 를 공부해보면 조건 동기화 관련해, producer-consumer, readers-writers, 식사하는 철학자 문제가 있는데 이를 공부해보면 좋다.)
쓰레드의 실행 순서같은 경우,
메모리에 값을 저장해놓는 쓰레드와 메모리에 값을 소비하는 역할의 쓰레드가 있다고 가정하자.
이 경우 전자의 쓰레드가 먼저 메모리에 접근하게끔 해야 하고, 이를 동기화를 통해 해결할 수 있다.
동기화 기법으로는 아래와 같은 방법이 있다.
- 뮤텍스
- 세마포어
MUTEX
임계영역에 들어갈 때, lock을 얻고서 들어가야 한다.
한 쓰레드가 임계 영역 안에서 작업중일 때, 다른 쓰레드는 밖에서 대기해야한다.
임계영역에서의 작업이 끝나면 lock을 풀어야 한다.
mutex를 일종의 자물쇠 시스템처럼 보는 것도 이해하기 좋다.
다음은 mutex의 생성 및 소멸함수이다.
#include <pthread.h> int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr); int pthread_mutex_destroy(pthread_mutex_t * mutex); //성공 시 0, 실패 시 0 이외의 값 리턴위 함수의 사용을 위해서는
pthread_mutex_t 형 변수가 하나 선언되어야 한다.
다음은 lock과 unlock에 대한 함수이다.
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); //성공 시 0, 실패 시 0 이외의 값 리턴pthread_mutex_lock 함수를 호출할 때, 이미 다른 쓰레드가 임계영역을 실행 중이라면,
pthread_mutex_lock 함수는 다른 쓰레드가 임계영역 실행을 끝내고 unlock을 하기 전까지
리턴을 하지 않는다.
다시 말해, blocking 상태에 놓인다!
사용방법은 아래와 같다.
pthread_mutex_lock(&mutex); //임계영역의 시작 //... //임계영역의 끝 pthread_mutex_unlock(&mutex);뮤텍스를 사용할 때에는 데드락에 빠지지 않도록 임계영역 끝에서 unlock을 해주어야 한다.
예제 (mutex.c)에서 임계영역의 범위를 어떻게 잡냐에 따라 속도 차이가 난다.
프로그램의 성격에 따라 임계영역을 넓게 잡을지, 최대한 좁게 잡을지 결정해줘야 한다.
세마포어
세마포어의 생성 및 소멸 함수는 아래와 같다.
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); int sem_destroy(sem_t *sem); //성공 시 0, 실패 시 0 이외의 값 리턴value=1이면 binary semaphore이다. 이는 mutex와 비슷한 역할(상호 배제)을 할 수 있게 된다.
뮤텍스의 lock, unlock에 대응되는 함수는 아래와 같다.
#include <semaphore.h> int sem_post(sem_t *sem); int sem_wait(sem_t *sem); //성공 시 0, 실패 시 0 이외의 값 리턴sem_init 함수가 호출되면 운영체제에 ‘세마포어 값’ 정수가 기록된다.
sem_post 호출 시 1 증가하고, sem_wait 호출 시 1 감소하게 된다.
세마포어값은 0보다 작아질 수 없어서 0인 상태에서
sem_wait을 호출하게 되면 리턴을 하지 않아 블로킹 상태에 놓이게 된다.
세마포어를 바탕으로 상호 배제를 하는 방법은 아래와 같다. ( OS를 꼭 공부하자 . )
(초기값이 1일 때이다. )
sem_wait(&sem); // 세마포어의 값을 0으로 // 임계 영역의 시작 // ... // 임계 영역의 끝 sem_post(&sem); // 세마포어의 값을 1로...
18-5 쓰레드의 소멸과 멀티쓰레드 기반의 다중접속 서버 구현
쓰레드를 소멸하는 방법으로는 다음과 경우가 있다.
- pthread_join 함수 호출
- pthread_detach 함수 호출
pthread_join 함수는 쓰레드가 종료할 때까지 blocking 되는 것이 가장 큰 문제다.
그래서 일반적으로는 pthread_detach함수를 사용해 쓰레드 소멸을 진행한다.
#include <pthread.h> int pthread_detach(pthread_t thread); //성공 시 0, 실패 시 0 이외의 값 리턴이 함수가 호출된 이후, 해당 쓰레드에서는 pthread_join함수 호출이 안된다!
아래는 멀티쓰레딩을 이용한 서버와 클라이언트 코드 예제 실행 결과이다.

'네트워크' 카테고리의 다른 글
On-Premise(온프레미스) 현장에서 실무 대화 이해하기 - 용어 정리편. (0) 2025.11.16 TCP/IP 소켓 프로그래밍 - 17장 : select 보다 나은 epoll (0) 2024.06.26 TCP/IP 소켓 프로그래밍 - 16장 : 입출력 스트림 분리 (0) 2024.06.26 TCP/IP 소켓 프로그래밍 - 15장 : 소켓과 표준 입출력 (0) 2024.06.25 TCP/IP 소켓 프로그래밍 - 14장 : 멀티캐스트 & 브로드캐스트 (0) 2024.06.25