다수의 클라이언트를 각 독립적으로 처리하는 실용적인 TCP서버를 작성하고자 할 때
대표적인 다중 처리 기법인 멀티스레드를 고려할 수 있다.
스레드 기본 개념
스레드란?
- 스레드(Thread)는 프로세스(Process) 내부에서 실행되는 단위
- 하나의 프로세스는 하나 이상의 스레드를 가질 수 있다
- 프로세스: 실행 중인 프로그램, 독립된 메모리 공간을 가짐 (코드, 데이터, 힙, 스택).
- 스레드: 프로세스 내부에서 실행 흐름을 나누는 단위, 같은 메모리 공간을 공유.
프로세스와 메모리 구조
- 프로세스 메모리 구조:
- 코드(Code) 영역: 실행 코드가 저장됨.
- 데이터(Data) 영역: 전역/정적 변수.
- 힙(Heap) 영역: 동적 메모리 할당.
- 스택(Stack) 영역: 함수 호출 및 지역 변수.
- 스레드 특징:
- 코드, 데이터, 힙 영역은 프로세스의 모든 스레드가 공유.
- 각 스레드는 독립적인 스택을 가짐.
스레드와 프로세스의 차이
| 정의 | 실행 중인 독립적인 프로그램 | 프로세스 내에서 실행 흐름의 단위 |
| 메모리 | 독립된 메모리 공간을 가짐 | 프로세스의 메모리를 공유 |
| 통신 | 프로세스 간 통신은 비용이 큼 | 메모리 공유로 인해 빠른 통신 가능 |
| 종속성 | 독립적 | 프로세스에 종속적 |
컨텍스트 스위칭(Context Switching):
- CPU가 여러 스레드를 번갈아가며 실행하는 과정.
- 스레드 상태(레지스터, 메모리 포인터)를 저장/복원.
멀티스레드와 CPU의 실행 원리
멀티스레드는 어떻게 실행되는가?
- 스레드 생성:
- 하나의 프로세스에서 여러 스레드를 생성.
- 각 스레드는 독립적인 실행 흐름을 가짐.
- CPU에서 실행:
- CPU는 한 번에 하나의 스레드만 실행 가능.
- 여러 스레드는 컨텍스트 스위칭을 통해 번갈아 실행.
스레드 API
스레드 생성과 종료
프로세스가 생성되면 main( ) 함수를 실행 시작점으로 하는 메인 스레드가 자동으로 생성된다.
이 떄 또 다른 함수인 어떤 함수를 실행 시작점으로 하는 스레드를 생성하려면
다음과 같은 정보를 운영체제에게 줘야한다.
-> 스레드의 시작점으로 설정할 함수 시작 주소
C프로그램에서는 함수 이름이 곧 그 함수의 시작 주소를 의미한다.
스레드 실행 시작점이 되는 함수를 스레드함수 라고 한다
-> 함수 실행 시 사용할 스택의 크기
- 리눅스 운영체제가 제공하는 함수, 시스템호출을 사용해야한다.
- C 프로그램의 모든 함수는 실행 중 인수전달과 변수 할당을 위해 스택이 필요하다.
실행에 필요한 스택 생성은 운영체제가 해주기 때문에 크기만 알려주면된다.
-> 만약 어떤 함수를 시작점으로 실행하는 두 개의 스레드를 생성하려고 한다면
서로 다른 메모리 위치에 총 두 개 의 스택을 할당해야한다.
스레드 생성
pthread_create( ) 함수
- 스레드 생성 함수
- 스레드를 생성한 후 스레드 식별자를 첫 번째 인수를 통해 반환
- 스레드 식별자는 파일,소켓 디스크립터와 비슷한 개념으로
운영체제의 스레드 관련 데이터 구조체를 간접적으로 참조하는 역할을한다. - 응용 프로그램은 스레드 식별자 (pthread_t 타입)를
시스템호출에 사용하여 다양한 방식으로 스레드를 제어할 수 있다.
C
#include <pthread.h>
int pthread_create(
① pthread_t *thread
② const pthread_attr_t *attr
③ void (*start_routine) (void *)
④ void *arg
);
1. 스레드 생성이 성공하면 스레드 식별자가 여기에 저장된다
2. 생성할 스레드의 속성을 제어한다.
기본값인 NULL 을 사용하면 문제없다.
3. 스레드 함수의 시작 주소다
스레드 함수 이름은 자유롭게 하되 형태는
void *threadMyCustom(void *arg) { } 입출력 타입을 정의해야한다. -> 매개변수와 리턴타입
4. 스레드 함수에 전달할 인수
포인터의 크기보다 작거나 같은 데이터는 값을 직접 넣거나 주소형태로 줘도된다.
하지만 포인터의 크기보다 큰 데이터는 구조체나 배열에 넣고 반드시 주소형태로 전달해야한다.
포인터의 크기는 운영체제 마다 정해져있다.
포인터보다 큰 데이터를 줘야할 경우에는 그 데이터의 주소를 참조하는 포인터를 만들어
그 포인터를 주면된다는 뜻임
ex) 구조체를 인수로 줘야한다면 구조체 포인터를 만들어서 구조체 포인터를 준다
스레드 종료
스레드를 종료하는 방법 4가지
- 스레드 함수가 리턴한다
- 스레드 함수 안에서 pthread_exit( ) 함수를 호출한다
- 다른 스레드가 pthread_cancel( ) 함수를 호출하여 스레드에 취소 요청을 보낸다.
- 메인 스레드가 종료하면 프로세스 내의 다른 모든 스레드가 강제 종료된다.
1번 또는 2번으로 종료하는 것이 바람직하다 .
3은 꼭 필요한 경우에만 사용하며
4는 정상종료라기 보단 메인스레드의 특성으로 이해하면 된다.
C
#include <pthread.h>
pthread_exit (
void *retval // 종료코드
);
pthread_cancle (
pthread_t thread; // 취소할 스레드의 식별자
);
스레드 제어
스레드 우선순위를 변경하거나 종료를 기다리는 등의 제어 기능을 제공한다.
스레드 우선순위 변경하기
스레드는 운영체제 내부로 들어가면 태스크 라고 불리는 실행 단위와 일대일 대응한다.
프로세스 내부의 스레드 하나하나가 운영체제의 내부의 태스크로 표현되며
이 태스크가 실질적인 실행 주체가 된다.
리눅스에서는 항상 여러 태스크가 CPU 시간을 사용하기 위해 경쟁한다.
각 태스크에 CPU시간을 적절히 분배하기 위한 정책을 사용하는데
이를 CPU 스케줄링, 테스크 스케줄링, 프로세스 스케줄링 등으로 부른다.
스케줄링 기법은 우선순위에 기반한 것으로 우선순위가 높은 태스크에 먼저 CPU 시간을 할당
태스크 우선순위 결정요소
스케줄링 정책
실시간과 정규정책이 있다.
-> 실시간 정책을 따르는 태스크는 정규 정책을 따르는 태스크 보다 항상 우선순위가 더 높다
스케줄링 우선순위
실시간 스케줄링 정책 : 1~99 범위의 우선순위를 지원한다
-> 고정된 우선순위가 있기때문에 일반 응용프로그램에 적합하지 않음
정규 스케줄링 정책 : 고정된 순위 지원 X
-> Nice 값을 이용하여 상대적인 우선순위를 조절할 수 있다
-> Nice값이 작을수록 우선순위가 높다.
nice( ) 함수
C
#include <unistd.h>
int nice( int inc)
- 함수호출 : 호출한 스레드의 우선순위 변경
- inc 값 : -20 ~ + 19 까지 가능
- 리턴값 : 새로운 Nice 값 (-1 일 수도 있음)
- 성공여부 : -1 은 대표적인 실패값이기 때문 구분하기 위해
호출 후 리턴값과 errno 값 모두 확인
스레드 종료 기다리기
- 스레드는 생성되면 CPU 시간을 사용하기위해 다른 스레드와 경쟁하면서 독립적으로 실행된다.
- 때로는 한스레드가 다른 스레드의 종료 여부, 즉 작업 완료 여부를 확인해야할 때가 있다.
- pthread_join( ) 함수를 사용하면 특정 스레드가 종료할 때까지 기다릴 수 있다.
C
#include <pthread.h>
int pthread_join ( ① pthread_t thread ② void** retval );
1. 종료를 기다릴 대상 스레드의 식별자
2. 스레드 함수의 리턴값을 받는 데 사용한다. 리턴값이 필요없으면 NULL 을 넣는다.
멀티스레드 TCP 서버
멀티스레드 TCP서버 기본 형태
void *processClient( void *arg ) {
// ③ 전달된 소켓 저장
SOCKET client_sock = (SOCKET) (long long) arg;
// ④ 클라이언트 정보 얻기
addrlien = sizeof(clientaddr);
getpeername(client_sock, (struct sockaddr *)&clientaddr, &addrlen);
// ⑤ 클라이언트와 데이터 통신
while(1) {
...
}
}
int main( int argc, char *argv[] ) {
...
while (1) {
// ① 클라이언트 접속 수용
client_sock = accept(listen_sock, ... );
...
// ② 스레드 생성
pthread_create(&tid, NULL, ProcessClient, (void*)(long long)client_sock);
}
}
1. 클라이언트가 접속하면 accept( ) 함수는 클라이언트와 통신할 수 있는 소켓을 리턴한다.
2. 클라이언트 통신을 담당할 스레드를 생성한다 이때 스레드 함수에 소켓을 넘겨준다.
리눅스 소켓은 정수형이므로 포인터형보다 크기가 작거나 같아 주소가 아닌 값으로 전달하는 것이 좋다.
3. 스레드 함수는 인수로 전달된 소켓을 소켓 타입(정수형)으로 변환하여 저장해둔다.
4. getpeername( )함수를 호출하여 클라이언트의 IP주소와 포트번호를 얻는다.
이코드는 필수는 아니며 클라이언트 정보 출력시 필요
5. 클라이언트와 데이터 통신
(long long)
컴파일 경고를 막기 위함 64비트환경에서 정수형(32비트)포인터형으로 캐스팅시 컴파일 경고
타입 캐스팅을 하지 않으면 포인터형을 정수형으로 변환하는 과정에서 컴파일 오류가 생김
스레드 함수에 전달한 경우에는 별도에 주소정보가 없다
-> 소켓 자체에서 주소 정보를 얻을 수 있는 소켓함수가 있다.
#include <sys/socket.h>
int getpeername(
int sock
struct sockaddr *addr
int *addrlen
)
소켓 데이터 구조체에 저장된 원격 IP 주소와 포트번호를 리턴
int getsockname(
int sock
struct sockaddr *addr
int *addrlen
)
소켓 데이터 구조체에 저장된 지역 IP 주소와 포트번호를 리턴
두 함수 모두
첫 번째 인수는 소켓
두 번째 인수는 소켓 주소 구조체
세 번째 인수는 소켓 주소 구조체의 크기
스레드 동기화
스레드 동기화 필요성

스레드 1이 ①을 수행한 상태에서 정지됨
- ① (메모리에서 데이터를 읽음)
스레드 1이 money 값을 읽어옵니다. 예를 들어, money 값이 현재 1000- 스레드 1은 money 값을 CPU 레지스터(예: ECX)에 복사
- 이 시점에서 money = 1000이 CPU 레지스터에 저장되어 있다.
2. 스레드 2가 ① ~ ③을 수행함
스레드 2는 money 값을 업데이트
- ① (메모리에서 데이터를 읽음)
스레드 2도 money 값을 읽습니다. 초기값은 여전히 1000- 스레드 2의 레지스터(예: ECX)에도 1000이 저장
- ② (값을 연산함)
스레드 2는 money에 4000을 더함.- 즉, ECX = 1000 + 4000 = 5000.
- ③ (메모리에 저장함)
스레드 2는 계산한 값을 메모리에 기록- 이제 메모리의 money = 5000.
3. 스레드 1이 다시 ② ~ ③을 수행함
스레드 1은 이전에 저장해 둔 레지스터 값(ECX = 1000)을 사용
- 이 시점에서 스레드 1은 스레드 2가 값을 업데이트한 사실을 모름
- ② (값을 연산함)
스레드 1은 레지스터 값에 2000을 더합니다.- ECX = 1000 + 2000 = 3000.
- ③ (메모리에 저장함)
스레드 1은 계산 결과(ECX = 3000)를 메모리에 기록합니다.- 이제 메모리의 money = 3000.
멀티스레드 환경에서 발생하는 이같은 문제를 해결하기 위한 작업을 스레드 동기화 라고 한다.
스레드 간에 값을 동기화하여 동시에 접근하거나 수정하지 못하도록 해야한다.
| 주요 특징 | 사욕 목적 | 장단점 | 관련함수(POSIX) |
뮤텍스 (Mutex)
| - 한 번에 하나의 스레드만 공유 자원에 접근 가능 - 잠금과 해제를 명시적으로 수행 |
- 데이터 보호 - 임계 구역 보호 |
장점: - 간단하고 효율적 - 단일 리소스 보호에 적합 단점: - 여러 스레드가 대기 시 성능 저하 - 데드락 위험 |
pthread_mutex_init pthread_mutex_lock pthread_mutex_unlock pthread_mutex_destroy |
읽기-쓰기 잠금
| - 다수의 스레드가 동시에 읽기 가능 - 쓰기는 한 번에 하나의 스레드만 가능 - 읽기와 쓰기가 상호 배타적 |
- 읽기 작업이 많고 쓰기 작업이 적은 경우 적합 | 장점: - 읽기-중심 작업에서 성능 향상 단점: - 구현이 뮤텍스보다 복잡 - 쓰기 스레드가 많은 경우 성능 저하 |
pthread_rwlock_init pthread_rwlock_rdlock pthread_rwlock_wrlock pthread_rwlock_unlock pthread_rwlock_destroy |
조건 변수
| - 특정 조건이 만족될 때까지 스레드를 대기 상태로 유지 - 다른 스레드가 신호(Signal)를 보내 조건을 만족시킴 |
- 이벤트 발생을 기다리는 작업에 사용 | 장점: - 효율적인 스레드 대기 관리 - 특정 조건에서만 동작 단점: - 뮤텍스와 함께 사용해야 하므로 구현이 복잡 - 조건이 충족되지 않는다면 불필요한 대기 가능 |
pthread_cond_init pthread_cond_wait pthread_cond_signal pthread_cond_broadcast pthread_cond_destroy |
배리어 (Barrier)
| - 여러 스레드가 특정 지점에서 모두 도달할 때까지 대기 - 모든 스레드가 도달하면 실행을 재개 |
- 다수의 스레드가 협력하여 특정 작업을 동시에 수행해야 하는 경우 | 장점: - 스레드 동기화 간소화 단점: - 유연성이 부족 - 일부 환경에서는 POSIX 표준 라이브러리에서 지원하지 않을 수 있음 |
pthread_barrier_init pthread_barrier_wait pthread_barrier_des |
스레드 동기화 기본 개념
필요한 상황
1. 둘 이상의 스레드가 공유 자원에 접근함
2. 한 스레드가 작업을 완료한 후 기다리는 다른 스레드에게 알려줌
두 가지 경우 모두 각 스레드가 독립적으로 실행하지 않고 다른 스레드와 상호작용을 토대로
자신의 작업을 진행한다는 특징이 있다.

스레드를 동기화 하려면 스레드가 상호 작용해야 하므로 그림 처럼 매개체가 필요하다.
두 스레드가 동시에 진행하면 안 되는 상황에서 매개체를 통해 진행 가능 여부를 판단해
자신의 실행을 계속할지 결정한다.
뮤텍스 Mutual Exclusion (상호배제)
두 개 이상의 스레드가 공유 자원에 접근할 때, 오직 한 스레드만 접근을 허용하는 상황에서 사용
뮤텍스란?
뮤텍스(Mutex)는 자물쇠
- 여러 스레드가 하나의 자원을 동시에 사용하려고 하면 문제가 생길 수 있으니, 뮤텍스를 사용해서 자원을 보호
- 스레드가 자원을 사용하려면 자물쇠를 열고, 다 쓰고 나면 자물쇠를 다시 잠가야 다른 스레드가 사용가능
사용예시
C
#include <stdio.h>
#include <pthread.h>
int balance = 0; // 공유 자원
pthread_mutex_t mutex; // 뮤텍스 선언
void* update_balance(void* arg) {
for (int i = 0; i < 5; i++) {
pthread_mutex_lock(&mutex); // ① 자물쇠 잠금
printf("스레드 %ld: 잔액 수정 전: %d\n", pthread_self(), balance);
balance += 1000; // ② 공유 자원 수정
printf("스레드 %ld: 잔액 수정 후: %d\n", pthread_self(), balance);
pthread_mutex_unlock(&mutex); // ③ 자물쇠 해제
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_mutex_init(&mutex, NULL); // 뮤텍스 초기화
pthread_create(&thread1, NULL, update_balance, NULL);
pthread_create(&thread2, NULL, update_balance, NULL);
pthread_join(thread1, NULL); // 스레드 1 종료 대기
pthread_join(thread2, NULL); // 스레드 2 종료 대기
pthread_mutex_destroy(&mutex); // 뮤텍스 소멸
printf("최종 잔액: %d\n", balance);
return 0;
}
Ex)
뮤텍스 사용 하지 않을 경우
스레드 1 | 스레드 2
------------ | --------------
balance += 1 | balance += 1
결과: 값이 꼬임 (Race Condition) - 경쟁 상태
뮤텍스 사용하면
스레드 1 | 스레드 2
------------ | --------------
lock(mutex) |
balance += 1 |
unlock(mutex) | wait (뮤텍스 잠금 중)
| lock(mutex)
| balance += 1
| unlock(mutex)
결과: 값이 정확히 수정됨
'리눅스 소켓 프로그래밍 TCPIP' 카테고리의 다른 글
| 소켓 옵션 (0) | 2024.12.17 |
|---|---|
| UDP 서버 - 클라이언트 (0) | 2024.12.16 |
| TCP 서버 - 클라이언트 (1) | 2024.12.13 |
| 소켓 주소 구조체 다루기 (3) | 2024.12.13 |
| 소켓 시작하기 (0) | 2024.12.12 |