1. 쓰레드(Thread)란 무엇인가?
쓰레드는 프로세스 내부에서 실제로 작업을 수행하는 실행 흐름의 단위다.
프로세스가 운영체제로부터 자원을 할당받는 작업의 단위라면, 쓰레드는 그 자원을 이용하여 실제로 코드를 실행하는 주체다.
멀티쓰레드의 필요성
하나의 프로그램 안에서 다음과 같은 작업들을 동시에 수행해야 할 때 멀티쓰레드 구조를 사용한다.
- 키보드 입력 대기
- 화면 인터페이스(UI) 갱신
- 네트워크 데이터 송수신
- 대용량 파일 읽기/쓰기
프로세스 vs 쓰레드
| 구분 | 프로세스 (Process) | 쓰레드 (Thread) |
| 정의 | 실행 중인 프로그램의 인스턴스 | 프로세스 내의 실행 흐름 |
| 메모리 | 독립된 메모리 공간을 가짐 | 프로세스 내 메모리를 공유 (Stack 제외) |
| 자원 공유 | IPC(Inter-Process Communication) 필요 | 직접 공유 가능 (전역 변수 등) |
| 생성 비용 | 높음 (무거움) | 낮음 (가벼움) |
| 영향도 | 독립적 | 하나의 쓰레드 오류가 전체에 파급 가능 |
[도식화: 프로세스와 쓰레드의 메모리 구조]

2. 쓰레드 사용의 장단점
🟢장점
- 응답성 향상: 시간이 오래 걸리는 작업을 별도 쓰레드에서 처리하면 메인 화면(UI)이 멈추지 않는다.
- 효율적인 자원 공유: 같은 프로세스 내의 데이터(전역 변수 등)를 복잡한 과정 없이 쉽게 공유할 수 있다.
- 경제성: 프로세스를 새로 만드는 것보다 쓰레드를 생성하고 컨텍스트 스위칭(Context Switching)하는 비용이 훨씬 저렴하다.
🔴단점
- 동기화 문제: 여러 쓰레드가 같은 데이터에 동시에 접근하면 충돌(Race Condition)이 발생할 수 있어 정교한 관리가 필요하다.
- 디버깅의 어려움: 실행 흐름이 여러 개이므로 오류 발생 시 추적이 어렵다.
- 안전성: 한 쓰레드에서 세그멘테이션 폴트(Segmentation Fault)가 발생하면 프로세스 전체가 종료될 수 있다.
3. C 프로그램에서의 쓰레드 사용 (pthread)
리눅스/유닉스 환경에서는 POSIX 쓰레드인 pthread 라이브러리를 주로 사용한다.
핵심 API
- pthread_create(): 새로운 쓰레드를 생성한다.
- pthread_join(): 특정 쓰레드가 종료될 때까지 대기한다.
- pthread_exit(): 실행 중인 쓰레드를 종료한다.
- pthread_self(): 현재 실행 중인 쓰레드의 ID를 반환한다.
컴파일 방법
쓰레드 라이브러리를 명시적으로 연결해주어야 하므로 -lpthread 옵션을 추가해야 한다.
gcc -o output source.c -lpthread
4. 실습 예제 분석 및 흐름 도식화
4가지 예제를 통해 쓰레드의 동작 방식을 단계별로 분석한다.
예제1️⃣: 쓰레드가 없는 경우 (Single Thread)
입력 함수(scanf)가 실행되면 사용자가 값을 입력할 때까지 프로그램의 모든 동작이 멈춘다(Blocking).
#include <stdio.h>
int main() {
int num;
printf("정수 입력: ");
scanf("%d", &num); // 여기서 프로그램이 멈춤
printf("입력한 값: %d\n", num);
return 0;
}

예제2️⃣: 기본적인 멀티쓰레드 구현
pthread_create를 사용하여 입력을 기다리는 동안에도 백그라운드에서 숫자를 출력하는 쓰레드를 생성한다.
// pthread1.c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_function(void *arg) {
sleep(5); // 5초 대기 후 시작
for(int i=1; i<=1000; i++) {
printf("thread_function: %d\n", i);
sleep(1);
}
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_function, NULL); // 쓰레드 생성
int num;
printf("정수 입력: ");
scanf("%d", &num); // 입력 대기 중에도 thread_function은 작동함
printf("입력한 값: %d\n", num);
return 0;
}
☑️ pthread_create 함수 원형
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- 첫 번째 인자: pthread_t *thread (쓰레드 식별자)
- 성공적으로 쓰레드가 생성되면, 해당 쓰레드를 가리키는 고유한 ID(식별자)가 이 변수에 저장된다.
- &tid가 이에 해당하며, 나중에 이 ID를 이용해 특정 쓰레드를 기다리거나(pthread_join) 종료할 수 있다.
- 두 번째 인자: const pthread_attr_t *attr (쓰레드 속성)
- 쓰레드의 특성을 설정하는 데 사용된다. (예: 스택 크기, 분리 상태 등)
- 대부분의 경우 기본 설정을 사용하기 위해 NULL을 전달한다.
- 세 번째 인자: void *(*start_routine) (void *) (시작 함수)
- 쓰레드가 생성되자마자 실행할 함수의 포인터를 전달한다.
- thread_function이 이에 해당한다. 즉, 새로운 쓰레드는 이 함수를 실행하게 된다.
- 이 함수는 반드시 void * 타입을 반환하고, void * 타입을 인자로 받는 형태여야 한다.
- 네 번째 인자: void *arg (함수에 전달할 인자)
- 세 번째 인자인 시작 함수에 전달하고 싶은 데이터가 있다면 이 인자를 통해 넘겨준다.
- 위 코드에서는 전달할 데이터가 없어서 NULL을 넣었다. 만약 특정 숫자나 구조체를 넘기고 싶다면 해당 주소값을 캐스팅하여 전달한다.

-lpthread 옵션을 주고 컴파일 후 실행한다.
gcc -o pthread1 pthread1.c -lpthread
./pthread1
프로그램을 실행하자마자 메인 쓰레드는 scanf에서 입력을 기다리며 멈춰(Blocking) 있다.
pthread_create로 만든 서브 쓰레드는 메인 쓰레드와 상관없이 자기 할 일을 수행한다.

Ctrl+Z를 눌러서 프로세스를 일시 중단하고 프로세스의 상태를 확인해본다.

다시 fg로 가져와서 값을 입력하고 프로세스를 종료한다.

예제3️⃣: 구조체를 이용한 인수 전달
쓰레드 함수에는 하나의 인자(void *arg)만 전달할 수 있다.
여러 데이터를 전달하려면 구조체로 묶어서 주소값을 넘겨야 한다.
// pthread2.c
// 스레드 예제2 (인수가 있는 경우)
#include <stdio.h> // printf, scanf, perror
#include <pthread.h> // pthread_create
#include <unistd.h> // sleep
void *thread_function(void *arg);
typedef struct {
int start; // 시작 숫자
int count; // 끝 숫자
int sleeptime; // 대기 시간
} ThreadArg;
int main()
{
int num;
pthread_t tid;
ThreadArg t_arg;
// 스레드에 전달할 값 설정
t_arg.start = 10;
t_arg.count = 30;
t_arg.sleeptime = 5;
if(pthread_create(&tid, NULL, thread_function, &t_arg) != 0)
{ // 스레드가 생성되지 않았다면 프로세스를 종료한다.
perror("pthread_create");
return 1;
}
printf("정수 입력: ");
scanf("%d", &num);
printf("입력한 값: %d\n", num);
// 스레드 종료 대기
// pthread_join(tid, NULL);
return 0;
}
void *thread_function(void *arg)
{
// 스레드 함수는 전달받은 인수는 void *arg를
// ThreadArg * 형식으로 형변환하여 각 값을 꺼내서 사용한다.
ThreadArg *data = (ThreadArg *)arg;
int i = data->start;
int count = data->count;
int sleeptime = data->sleeptime;
sleep(sleeptime);
while(i <= count)
{
printf("thread_function: %d\n", i++);
sleep(1);
}
return NULL;
}
1. 데이터 묶기: ThreadArg 구조체
typedef struct { ... } ThreadArg; :
pthread_create 함수의 네 번째 인자는 딱 한 개만 들어갈 수 있다.
위 코드에서는 시작값, 끝값, 대기시간이라는 3개의 데이터를 전달해야 한기 때문에 구조체를 하나 만들어서 그 안에 세 개의 데이터를 모두 집어넣어야 한다.
2. 전달하기: main 함수에서의 설정
메인 함수는 쓰레드를 만들기 전, 구조체에 구체적인 데이터를 채운다.
- t_arg.start = 10; t_arg.count = 30; t_arg.sleeptime = 5; : 실제 값을 설정한다.
- 주소 전달: pthread_create(&tid, NULL, thread_function, &t_arg)
- 여기서 &t_arg를 넘겨주는 것은 t_arg로 선언된 구조체 자체를 복사해서 주는 게 아니라, 그 주소를 전달해주는 방식이다.
3. 꺼내 쓰기: thread_function에서의 변환
쓰레드 함수 입장에서는 인자를 받을 때 void *arg라는 "정체를 알 수 없는 주소" 형태로 받게 된다.
따라서 이를 다시 원래 구조체 모양으로 인식시켜야 한다.
- 형변환(Casting): ThreadArg *data = (ThreadArg *)arg;
- 이 void * 주소가 ThreadArg 타입의 데이터가 있는 위치라고 컴퓨터에게 알려주는 것이다.
- 값 추출: data->start와 같이 화살표(->) 연산자를 사용하여 구조체 안의 데이터를 하나씩 꺼내서 변수(i, count 등)에 담는다.

-lpthread 옵션을 주고 컴파일 후 실행한다.
gcc -o pthread2 pthread2.c -lpthread
./pthread2

예제4️⃣: 실행 인자(CLI)를 통한 쓰레드 제어
프로그램 실행 시 CLI에서 전달받은 argv 값을 정수로 변환하여 쓰레드의 동작(시작값, 끝값, 대기시간)을 결정한다.
// pthread3.c
// 스레드 예제3 (인수가 있고 main() 에서 인수로 받는 경우)
#include <stdio.h> // printf, scanf, perror
#include <pthread.h> // pthread_create
#include <unistd.h> // sleep
#include <stdlib.h> // atoi
void *thread_function(void *arg);
typedef struct {
int start; // 시작 숫자
int count; // 끝 숫자
int sleeptime; // 대기 시간
} ThreadArg;
int main(int argc, char **argv)
{
int num;
int start;
int count;
int sleeptime;
pthread_t tid;
ThreadArg t_arg;
if(argc != 4) {
fprintf(stderr, "Usage %s start count sleeptime\n"
"e.g.) %s 10 30 5 \n", argv[0], argv[0]);
return 1;
}
start = atoi(argv[1]);
count = atoi(argv[2]);
sleeptime = atoi(argv[3]);
if(start > count) {
fprintf(stderr, "start 가 count 보다 작아야 합니다.\n");
return 2;
}
// 스레드에 전달할 값 설정
t_arg.start = start;
t_arg.count = count;
t_arg.sleeptime = sleeptime;
if(pthread_create(&tid, NULL, thread_function, &t_arg) != 0)
{ // 스레드가 생성되지 않았다면 프로세스를 종료한다.
perror("pthread_create");
return 1;
}
printf("정수 입력: ");
scanf("%d", &num);
printf("입력한 값: %d\n", num);
// 스레드 종료 대기
pthread_join(tid, NULL);
return 0;
}
void *thread_function(void *arg)
{
// 스레드 함수는 전달받은 인수는 void *arg를
// ThreadArg * 형식으로 형변환하여 각 값을 꺼내서 사용한다.
ThreadArg *data = (ThreadArg *)arg;
int i = data->start;
int count = data->count;
int sleeptime = data->sleeptime;
sleep(sleeptime);
while(i <= count) {
printf("thread_function: %d\n", i++);
sleep(1);
}
return NULL;
}
1. 실행 인자(CLI) 처리 및 예외 관리
main(int argc, char argv)를 통해 프로그램 실행 시 값을 입력받는다.
- 유효성 검사: argc != 4 체크를 통해 인자가 부족하면 사용법(Usage)을 안내하고 종료한다.
- 데이터 변환: atoi()를 사용하여 문자열 형태의 인자를 정수로 변환한다.
- 논리적 에러 방지: 시작값(start)이 끝값(count)보다 크면 실행되지 않도록 차단한다.
2. 구조체를 활용한 데이터 전달
pthread_create는 쓰레드 함수에 단 하나의 포인터(void *)만 넘길 수 있으므로 여러 데이터(start, count, sleeptime)를 ThreadArg라는 구조체로 묶고, &t_arg(구조체의 주소)를 네 번째 인자로 넘겨서 서브 쓰레드가 메인 쓰레드의 메모리 영역에 있는 데이터를 참조하게 한다.
3. 쓰레드 내부에서의 형변환 (Casting)
서브 쓰레드 함수(thread_function)는 인자를 void * 타입으로 받는다.
이를 다시 원래의 타입인 ThreadArg *로 형변환(data = (ThreadArg *)arg)해야만 구조체 내부의 멤버 변수들에 접근할 수 있다.
4. pthread_join
main 함수가 scanf 이후 바로 종료되면, pthread_join을 호출하지 않았을 때 서브 쓰레드가 남은 작업을 다 하지 못하고 프로세스와 함께 소멸할 수 있다.
안정적인 프로그램을 위해서는 pthread_join을 통한 쓰레드 종료 동기화가 필수적이다.

-lpthread 옵션을 주고 컴파일한다.
gcc -o pthread3 pthread3.c -lpthread
먼저 인수가 없이 실행해본다.

사용법이 출력되고 프로세스가 종료된다.
이번에는 인수로 시작 숫자, 끝 숫자와 대기 시간을 입력해서 실행한다.

scanf를 실행했지만 pthread_join 함수가 호출되었기 때문에 프로세스가 바로 종료되지 않고 서브 쓰레드가 끝날 때 종료되는 것을 볼 수 있다.
'Journey to Security > 리눅스' 카테고리의 다른 글
| OpenSSL로 Apache HTTPS 인증서 생성하기 (2) 루트 인증서 등록 (0) | 2026.05.01 |
|---|---|
| OpenSSL로 Apache HTTPS 인증서 생성하기 (1) SAN, 와일드카드 인증서 (0) | 2026.04.30 |
| [서버 보안] 리눅스 SSH 백도어 점검 및 삭제하기 (0) | 2026.04.26 |
| [서버 보안] Brute Force 공격과 무단 공개키 삽입(SSH 백도어) (0) | 2026.04.23 |
| 리눅스 파티션 분할로 서버 보안과 안정성 챙기기 (+Rocky Linux 9 파티션 분할 설치 방법) (0) | 2026.04.19 |