프로젝트 12: 멀티스레드 프로그래밍
프로젝트 12: 멀티스레드 프로그래밍¶
pthread 라이브러리를 사용한 멀티스레드 프로그래밍을 배웁니다.
학습 목표¶
- 스레드 생성과 관리
- 뮤텍스를 이용한 동기화
- 조건 변수 사용
- 생산자-소비자 패턴 구현
사전 지식¶
- 포인터
- 구조체
- 함수 포인터
1단계: 스레드 기초¶
첫 번째 스레드 프로그램¶
// thread_basic.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
// 스레드 함수: void* 반환, void* 인자
void* print_message(void* arg) {
char* message = (char*)arg;
for (int i = 0; i < 5; i++) {
printf("[스레드] %s - %d\n", message, i);
sleep(1);
}
return NULL;
}
int main(void) {
pthread_t thread;
const char* msg = "Hello from thread";
// 스레드 생성
int result = pthread_create(&thread, NULL, print_message, (void*)msg);
if (result != 0) {
fprintf(stderr, "스레드 생성 실패: %d\n", result);
return 1;
}
// 메인 스레드도 작업 수행
for (int i = 0; i < 5; i++) {
printf("[메인] Main thread - %d\n", i);
sleep(1);
}
// 스레드 종료 대기
pthread_join(thread, NULL);
printf("모든 작업 완료\n");
return 0;
}
컴파일¶
# Linux
gcc -o thread_basic thread_basic.c -pthread
# macOS
gcc -o thread_basic thread_basic.c -lpthread
여러 스레드 생성¶
// multi_threads.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREADS 5
// 스레드에 전달할 데이터
typedef struct {
int id;
char name[32];
} ThreadData;
void* thread_func(void* arg) {
ThreadData* data = (ThreadData*)arg;
printf("스레드 %d (%s) 시작\n", data->id, data->name);
// 작업 시뮬레이션
int sum = 0;
for (int i = 0; i < 1000000; i++) {
sum += i;
}
printf("스레드 %d 완료: sum = %d\n", data->id, sum);
return NULL;
}
int main(void) {
pthread_t threads[NUM_THREADS];
ThreadData data[NUM_THREADS];
// 스레드 생성
for (int i = 0; i < NUM_THREADS; i++) {
data[i].id = i;
snprintf(data[i].name, sizeof(data[i].name), "Worker-%d", i);
int result = pthread_create(&threads[i], NULL, thread_func, &data[i]);
if (result != 0) {
fprintf(stderr, "스레드 %d 생성 실패\n", i);
exit(1);
}
}
printf("모든 스레드 생성 완료. 대기 중...\n");
// 모든 스레드 대기
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("프로그램 종료\n");
return 0;
}
스레드 반환값 받기¶
// thread_return.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void* calculate_sum(void* arg) {
int n = *(int*)arg;
// 동적 할당하여 결과 반환
long* result = malloc(sizeof(long));
*result = 0;
for (int i = 1; i <= n; i++) {
*result += i;
}
printf("스레드: 1부터 %d까지 합 계산 완료\n", n);
return result;
}
int main(void) {
pthread_t thread;
int n = 100;
pthread_create(&thread, NULL, calculate_sum, &n);
// 반환값 받기
void* ret_val;
pthread_join(thread, &ret_val);
long* result = (long*)ret_val;
printf("결과: %ld\n", *result);
free(result); // 동적 할당된 메모리 해제
return 0;
}
2단계: 경쟁 조건 (Race Condition)¶
여러 스레드가 동시에 공유 데이터에 접근하면 문제가 발생합니다.
경쟁 조건 예제¶
// race_condition.c
#include <stdio.h>
#include <pthread.h>
#define NUM_THREADS 10
#define ITERATIONS 100000
// 공유 변수
int counter = 0;
void* increment(void* arg) {
(void)arg;
for (int i = 0; i < ITERATIONS; i++) {
counter++; // 원자적이지 않음!
// 실제로는: temp = counter; temp = temp + 1; counter = temp;
}
return NULL;
}
int main(void) {
pthread_t threads[NUM_THREADS];
// 스레드 생성
for (int i = 0; i < NUM_THREADS; i++) {
pthread_create(&threads[i], NULL, increment, NULL);
}
// 대기
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
// 예상: NUM_THREADS * ITERATIONS = 1,000,000
// 실제: 그보다 적은 값 (경쟁 조건으로 인한 손실)
printf("예상값: %d\n", NUM_THREADS * ITERATIONS);
printf("실제값: %d\n", counter);
printf("손실: %d\n", NUM_THREADS * ITERATIONS - counter);
return 0;
}
실행 결과:
예상값: 1000000
실제값: 847293
손실: 152707
3단계: 뮤텍스 (Mutex)¶
뮤텍스로 공유 자원에 대한 접근을 동기화합니다.
뮤텍스 사용¶
// mutex_example.c
#include <stdio.h>
#include <pthread.h>
#define NUM_THREADS 10
#define ITERATIONS 100000
int counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* increment_safe(void* arg) {
(void)arg;
for (int i = 0; i < ITERATIONS; i++) {
pthread_mutex_lock(&mutex); // 잠금
counter++; // 임계 구역
pthread_mutex_unlock(&mutex); // 해제
}
return NULL;
}
int main(void) {
pthread_t threads[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
pthread_create(&threads[i], NULL, increment_safe, NULL);
}
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("예상값: %d\n", NUM_THREADS * ITERATIONS);
printf("실제값: %d\n", counter);
pthread_mutex_destroy(&mutex);
return 0;
}
뮤텍스를 이용한 은행 계좌¶
// bank_account.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
typedef struct {
int balance;
pthread_mutex_t lock;
} Account;
Account* account_create(int initial_balance) {
Account* acc = malloc(sizeof(Account));
acc->balance = initial_balance;
pthread_mutex_init(&acc->lock, NULL);
return acc;
}
void account_destroy(Account* acc) {
pthread_mutex_destroy(&acc->lock);
free(acc);
}
int account_deposit(Account* acc, int amount) {
pthread_mutex_lock(&acc->lock);
acc->balance += amount;
int new_balance = acc->balance;
pthread_mutex_unlock(&acc->lock);
return new_balance;
}
int account_withdraw(Account* acc, int amount) {
pthread_mutex_lock(&acc->lock);
if (acc->balance >= amount) {
acc->balance -= amount;
int new_balance = acc->balance;
pthread_mutex_unlock(&acc->lock);
return new_balance;
}
pthread_mutex_unlock(&acc->lock);
return -1; // 잔액 부족
}
int account_get_balance(Account* acc) {
pthread_mutex_lock(&acc->lock);
int balance = acc->balance;
pthread_mutex_unlock(&acc->lock);
return balance;
}
// 이체 (두 계좌 간)
int account_transfer(Account* from, Account* to, int amount) {
// 데드락 방지: 항상 같은 순서로 잠금
// 주소값이 작은 계좌 먼저 잠금
Account* first = (from < to) ? from : to;
Account* second = (from < to) ? to : from;
pthread_mutex_lock(&first->lock);
pthread_mutex_lock(&second->lock);
int result = -1;
if (from->balance >= amount) {
from->balance -= amount;
to->balance += amount;
result = from->balance;
}
pthread_mutex_unlock(&second->lock);
pthread_mutex_unlock(&first->lock);
return result;
}
// 테스트용 스레드 데이터
typedef struct {
Account* acc;
int thread_id;
} ThreadArg;
void* depositor(void* arg) {
ThreadArg* ta = (ThreadArg*)arg;
for (int i = 0; i < 100; i++) {
int new_balance = account_deposit(ta->acc, 100);
printf("[입금자 %d] 입금 100원 -> 잔액: %d\n", ta->thread_id, new_balance);
usleep(rand() % 10000);
}
return NULL;
}
void* withdrawer(void* arg) {
ThreadArg* ta = (ThreadArg*)arg;
for (int i = 0; i < 100; i++) {
int result = account_withdraw(ta->acc, 100);
if (result >= 0) {
printf("[출금자 %d] 출금 100원 -> 잔액: %d\n", ta->thread_id, result);
} else {
printf("[출금자 %d] 잔액 부족\n", ta->thread_id);
}
usleep(rand() % 10000);
}
return NULL;
}
int main(void) {
srand(time(NULL));
Account* acc = account_create(10000);
printf("초기 잔액: %d\n\n", account_get_balance(acc));
pthread_t depositors[3];
pthread_t withdrawers[3];
ThreadArg args[6];
// 입금자 3명
for (int i = 0; i < 3; i++) {
args[i].acc = acc;
args[i].thread_id = i;
pthread_create(&depositors[i], NULL, depositor, &args[i]);
}
// 출금자 3명
for (int i = 0; i < 3; i++) {
args[i + 3].acc = acc;
args[i + 3].thread_id = i;
pthread_create(&withdrawers[i], NULL, withdrawer, &args[i + 3]);
}
// 대기
for (int i = 0; i < 3; i++) {
pthread_join(depositors[i], NULL);
pthread_join(withdrawers[i], NULL);
}
printf("\n최종 잔액: %d\n", account_get_balance(acc));
printf("예상 잔액: %d (초기 10000 + 입금 30000 - 출금 최대 30000)\n", 10000);
account_destroy(acc);
return 0;
}
4단계: 조건 변수 (Condition Variable)¶
특정 조건이 만족될 때까지 스레드를 대기시킵니다.
조건 변수 기본¶
// condition_basic.c
#include <stdio.h>
#include <pthread.h>
#include <stdbool.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
bool ready = false;
void* waiter(void* arg) {
int id = *(int*)arg;
pthread_mutex_lock(&mutex);
while (!ready) { // 조건이 false인 동안 대기
printf("[대기자 %d] 조건 대기 중...\n", id);
pthread_cond_wait(&cond, &mutex); // 대기 (뮤텍스 해제됨)
}
// pthread_cond_wait에서 깨어나면 뮤텍스 다시 획득됨
printf("[대기자 %d] 조건 만족! 작업 시작\n", id);
pthread_mutex_unlock(&mutex);
return NULL;
}
void* signaler(void* arg) {
(void)arg;
sleep(2); // 2초 대기
pthread_mutex_lock(&mutex);
ready = true;
printf("[신호자] 조건 설정 완료. 신호 전송!\n");
pthread_cond_broadcast(&cond); // 모든 대기자에게 신호
pthread_mutex_unlock(&mutex);
return NULL;
}
int main(void) {
pthread_t waiters[3];
pthread_t sig;
int ids[] = {1, 2, 3};
// 대기 스레드 생성
for (int i = 0; i < 3; i++) {
pthread_create(&waiters[i], NULL, waiter, &ids[i]);
}
// 신호 스레드 생성
pthread_create(&sig, NULL, signaler, NULL);
// 대기
for (int i = 0; i < 3; i++) {
pthread_join(waiters[i], NULL);
}
pthread_join(sig, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
5단계: 생산자-소비자 패턴¶
가장 중요한 동기화 패턴 중 하나입니다.
경계 버퍼 (Bounded Buffer)¶
// producer_consumer.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <stdbool.h>
#define BUFFER_SIZE 5
#define NUM_ITEMS 20
// 경계 버퍼
typedef struct {
int buffer[BUFFER_SIZE];
int count; // 현재 아이템 수
int in; // 다음 삽입 위치
int out; // 다음 추출 위치
pthread_mutex_t mutex;
pthread_cond_t not_full; // 버퍼가 가득 차지 않음
pthread_cond_t not_empty; // 버퍼가 비어있지 않음
bool done; // 생산 완료 플래그
} BoundedBuffer;
BoundedBuffer* buffer_create(void) {
BoundedBuffer* bb = malloc(sizeof(BoundedBuffer));
bb->count = 0;
bb->in = 0;
bb->out = 0;
bb->done = false;
pthread_mutex_init(&bb->mutex, NULL);
pthread_cond_init(&bb->not_full, NULL);
pthread_cond_init(&bb->not_empty, NULL);
return bb;
}
void buffer_destroy(BoundedBuffer* bb) {
pthread_mutex_destroy(&bb->mutex);
pthread_cond_destroy(&bb->not_full);
pthread_cond_destroy(&bb->not_empty);
free(bb);
}
void buffer_put(BoundedBuffer* bb, int item) {
pthread_mutex_lock(&bb->mutex);
// 버퍼가 가득 찼으면 대기
while (bb->count == BUFFER_SIZE) {
printf("[생산자] 버퍼 가득 참. 대기...\n");
pthread_cond_wait(&bb->not_full, &bb->mutex);
}
// 아이템 삽입
bb->buffer[bb->in] = item;
bb->in = (bb->in + 1) % BUFFER_SIZE;
bb->count++;
printf("[생산자] 아이템 %d 생산 (버퍼: %d/%d)\n",
item, bb->count, BUFFER_SIZE);
// 소비자에게 알림
pthread_cond_signal(&bb->not_empty);
pthread_mutex_unlock(&bb->mutex);
}
int buffer_get(BoundedBuffer* bb, int* item) {
pthread_mutex_lock(&bb->mutex);
// 버퍼가 비어있고 생산 완료 아니면 대기
while (bb->count == 0 && !bb->done) {
printf("[소비자] 버퍼 비어있음. 대기...\n");
pthread_cond_wait(&bb->not_empty, &bb->mutex);
}
// 버퍼가 비어있고 생산 완료면 종료
if (bb->count == 0 && bb->done) {
pthread_mutex_unlock(&bb->mutex);
return 0; // 더 이상 아이템 없음
}
// 아이템 추출
*item = bb->buffer[bb->out];
bb->out = (bb->out + 1) % BUFFER_SIZE;
bb->count--;
printf("[소비자] 아이템 %d 소비 (버퍼: %d/%d)\n",
*item, bb->count, BUFFER_SIZE);
// 생산자에게 알림
pthread_cond_signal(&bb->not_full);
pthread_mutex_unlock(&bb->mutex);
return 1; // 성공
}
void buffer_set_done(BoundedBuffer* bb) {
pthread_mutex_lock(&bb->mutex);
bb->done = true;
pthread_cond_broadcast(&bb->not_empty); // 모든 소비자 깨움
pthread_mutex_unlock(&bb->mutex);
}
// 생산자 스레드
void* producer(void* arg) {
BoundedBuffer* bb = (BoundedBuffer*)arg;
for (int i = 1; i <= NUM_ITEMS; i++) {
usleep((rand() % 500) * 1000); // 0~500ms 대기
buffer_put(bb, i);
}
printf("[생산자] 생산 완료\n");
buffer_set_done(bb);
return NULL;
}
// 소비자 스레드
void* consumer(void* arg) {
BoundedBuffer* bb = (BoundedBuffer*)arg;
int item;
while (buffer_get(bb, &item)) {
usleep((rand() % 800) * 1000); // 0~800ms 처리 시간
}
printf("[소비자] 소비 완료\n");
return NULL;
}
int main(void) {
srand(time(NULL));
BoundedBuffer* bb = buffer_create();
pthread_t prod;
pthread_t cons[2];
// 생산자 1명
pthread_create(&prod, NULL, producer, bb);
// 소비자 2명
pthread_create(&cons[0], NULL, consumer, bb);
pthread_create(&cons[1], NULL, consumer, bb);
// 대기
pthread_join(prod, NULL);
pthread_join(cons[0], NULL);
pthread_join(cons[1], NULL);
buffer_destroy(bb);
printf("\n프로그램 종료\n");
return 0;
}
6단계: 스레드 풀 (Thread Pool)¶
실제 서버 프로그램에서 많이 사용하는 패턴입니다.
스레드 풀 구현¶
// thread_pool.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdbool.h>
#include <unistd.h>
#define POOL_SIZE 4
#define QUEUE_SIZE 100
// 작업 정의
typedef struct Task {
void (*function)(void* arg);
void* arg;
} Task;
// 작업 큐
typedef struct {
Task tasks[QUEUE_SIZE];
int front;
int rear;
int count;
pthread_mutex_t mutex;
pthread_cond_t not_empty;
pthread_cond_t not_full;
bool shutdown;
} TaskQueue;
// 스레드 풀
typedef struct {
pthread_t threads[POOL_SIZE];
TaskQueue queue;
int thread_count;
} ThreadPool;
// 작업 큐 초기화
void queue_init(TaskQueue* q) {
q->front = 0;
q->rear = 0;
q->count = 0;
q->shutdown = false;
pthread_mutex_init(&q->mutex, NULL);
pthread_cond_init(&q->not_empty, NULL);
pthread_cond_init(&q->not_full, NULL);
}
// 작업 큐 정리
void queue_destroy(TaskQueue* q) {
pthread_mutex_destroy(&q->mutex);
pthread_cond_destroy(&q->not_empty);
pthread_cond_destroy(&q->not_full);
}
// 작업 추가
bool queue_push(TaskQueue* q, Task task) {
pthread_mutex_lock(&q->mutex);
while (q->count == QUEUE_SIZE && !q->shutdown) {
pthread_cond_wait(&q->not_full, &q->mutex);
}
if (q->shutdown) {
pthread_mutex_unlock(&q->mutex);
return false;
}
q->tasks[q->rear] = task;
q->rear = (q->rear + 1) % QUEUE_SIZE;
q->count++;
pthread_cond_signal(&q->not_empty);
pthread_mutex_unlock(&q->mutex);
return true;
}
// 작업 가져오기
bool queue_pop(TaskQueue* q, Task* task) {
pthread_mutex_lock(&q->mutex);
while (q->count == 0 && !q->shutdown) {
pthread_cond_wait(&q->not_empty, &q->mutex);
}
if (q->count == 0 && q->shutdown) {
pthread_mutex_unlock(&q->mutex);
return false;
}
*task = q->tasks[q->front];
q->front = (q->front + 1) % QUEUE_SIZE;
q->count--;
pthread_cond_signal(&q->not_full);
pthread_mutex_unlock(&q->mutex);
return true;
}
// 워커 스레드 함수
void* worker_thread(void* arg) {
ThreadPool* pool = (ThreadPool*)arg;
Task task;
printf("[워커] 스레드 시작 (TID: %lu)\n", pthread_self());
while (queue_pop(&pool->queue, &task)) {
printf("[워커 %lu] 작업 실행\n", pthread_self());
task.function(task.arg);
}
printf("[워커 %lu] 스레드 종료\n", pthread_self());
return NULL;
}
// 스레드 풀 생성
ThreadPool* pool_create(int size) {
ThreadPool* pool = malloc(sizeof(ThreadPool));
pool->thread_count = size;
queue_init(&pool->queue);
for (int i = 0; i < size; i++) {
pthread_create(&pool->threads[i], NULL, worker_thread, pool);
}
return pool;
}
// 작업 제출
bool pool_submit(ThreadPool* pool, void (*function)(void*), void* arg) {
Task task = { .function = function, .arg = arg };
return queue_push(&pool->queue, task);
}
// 스레드 풀 종료
void pool_shutdown(ThreadPool* pool) {
pthread_mutex_lock(&pool->queue.mutex);
pool->queue.shutdown = true;
pthread_cond_broadcast(&pool->queue.not_empty);
pthread_mutex_unlock(&pool->queue.mutex);
for (int i = 0; i < pool->thread_count; i++) {
pthread_join(pool->threads[i], NULL);
}
queue_destroy(&pool->queue);
free(pool);
}
// ============ 테스트 ============
typedef struct {
int id;
int value;
} WorkItem;
void process_work(void* arg) {
WorkItem* item = (WorkItem*)arg;
printf("작업 %d 처리 중 (값: %d)...\n", item->id, item->value);
usleep((rand() % 500 + 100) * 1000); // 100~600ms 처리
printf("작업 %d 완료!\n", item->id);
free(item);
}
int main(void) {
srand(time(NULL));
printf("스레드 풀 생성 (크기: %d)\n\n", POOL_SIZE);
ThreadPool* pool = pool_create(POOL_SIZE);
// 작업 제출
for (int i = 0; i < 10; i++) {
WorkItem* item = malloc(sizeof(WorkItem));
item->id = i;
item->value = rand() % 100;
printf("작업 %d 제출 (값: %d)\n", i, item->value);
pool_submit(pool, process_work, item);
usleep(100000); // 100ms 간격
}
printf("\n모든 작업 제출 완료. 풀 종료 대기...\n\n");
sleep(2); // 작업 처리 대기
pool_shutdown(pool);
printf("\n프로그램 종료\n");
return 0;
}
7단계: 읽기-쓰기 잠금 (Read-Write Lock)¶
읽기는 동시에, 쓰기는 배타적으로 허용합니다.
// rwlock_example.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define NUM_READERS 5
#define NUM_WRITERS 2
// 공유 데이터
typedef struct {
int data;
pthread_rwlock_t lock;
} SharedData;
SharedData shared = { .data = 0 };
void* reader(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 5; i++) {
pthread_rwlock_rdlock(&shared.lock); // 읽기 잠금
printf("[독자 %d] 데이터 읽음: %d\n", id, shared.data);
usleep(100000); // 읽기 중...
pthread_rwlock_unlock(&shared.lock);
usleep(rand() % 200000);
}
return NULL;
}
void* writer(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 3; i++) {
pthread_rwlock_wrlock(&shared.lock); // 쓰기 잠금 (배타적)
shared.data = rand() % 1000;
printf("[작가 %d] 데이터 씀: %d\n", id, shared.data);
usleep(200000); // 쓰기 중...
pthread_rwlock_unlock(&shared.lock);
usleep(rand() % 500000);
}
return NULL;
}
int main(void) {
srand(time(NULL));
pthread_rwlock_init(&shared.lock, NULL);
pthread_t readers[NUM_READERS];
pthread_t writers[NUM_WRITERS];
int reader_ids[NUM_READERS];
int writer_ids[NUM_WRITERS];
// 독자 생성
for (int i = 0; i < NUM_READERS; i++) {
reader_ids[i] = i;
pthread_create(&readers[i], NULL, reader, &reader_ids[i]);
}
// 작가 생성
for (int i = 0; i < NUM_WRITERS; i++) {
writer_ids[i] = i;
pthread_create(&writers[i], NULL, writer, &writer_ids[i]);
}
// 대기
for (int i = 0; i < NUM_READERS; i++) {
pthread_join(readers[i], NULL);
}
for (int i = 0; i < NUM_WRITERS; i++) {
pthread_join(writers[i], NULL);
}
pthread_rwlock_destroy(&shared.lock);
printf("완료\n");
return 0;
}
8단계: 실전 예제 - 병렬 정렬¶
멀티스레드 병합 정렬¶
// parallel_sort.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <time.h>
#define THRESHOLD 10000 // 이보다 작으면 단일 스레드
typedef struct {
int* arr;
int left;
int right;
} SortTask;
// 병합
void merge(int* arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int* L = malloc(n1 * sizeof(int));
int* R = malloc(n2 * sizeof(int));
memcpy(L, arr + left, n1 * sizeof(int));
memcpy(R, arr + mid + 1, n2 * sizeof(int));
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
arr[k++] = (L[i] <= R[j]) ? L[i++] : R[j++];
}
while (i < n1) arr[k++] = L[i++];
while (j < n2) arr[k++] = R[j++];
free(L);
free(R);
}
// 단일 스레드 병합 정렬
void merge_sort_single(int* arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
merge_sort_single(arr, left, mid);
merge_sort_single(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
// 멀티스레드 병합 정렬
void* merge_sort_parallel(void* arg) {
SortTask* task = (SortTask*)arg;
int* arr = task->arr;
int left = task->left;
int right = task->right;
if (left >= right) return NULL;
// 작은 배열은 단일 스레드로
if (right - left < THRESHOLD) {
merge_sort_single(arr, left, right);
return NULL;
}
int mid = left + (right - left) / 2;
// 왼쪽 절반: 새 스레드
SortTask left_task = { arr, left, mid };
pthread_t left_thread;
pthread_create(&left_thread, NULL, merge_sort_parallel, &left_task);
// 오른쪽 절반: 현재 스레드
SortTask right_task = { arr, mid + 1, right };
merge_sort_parallel(&right_task);
// 왼쪽 스레드 대기
pthread_join(left_thread, NULL);
// 병합
merge(arr, left, mid, right);
return NULL;
}
// 배열 출력
void print_array(int* arr, int n) {
for (int i = 0; i < n && i < 20; i++) {
printf("%d ", arr[i]);
}
if (n > 20) printf("...");
printf("\n");
}
// 배열 검증
int is_sorted(int* arr, int n) {
for (int i = 1; i < n; i++) {
if (arr[i] < arr[i - 1]) return 0;
}
return 1;
}
int main(void) {
srand(time(NULL));
int n = 1000000; // 백만 개
int* arr1 = malloc(n * sizeof(int));
int* arr2 = malloc(n * sizeof(int));
// 랜덤 배열 생성
for (int i = 0; i < n; i++) {
arr1[i] = rand();
arr2[i] = arr1[i]; // 복사
}
printf("배열 크기: %d\n\n", n);
// 단일 스레드 정렬
clock_t start = clock();
merge_sort_single(arr1, 0, n - 1);
clock_t end = clock();
double single_time = (double)(end - start) / CLOCKS_PER_SEC;
printf("단일 스레드: %.3f초\n", single_time);
printf("정렬 검증: %s\n\n", is_sorted(arr1, n) ? "OK" : "FAIL");
// 멀티스레드 정렬
start = clock();
SortTask task = { arr2, 0, n - 1 };
merge_sort_parallel(&task);
end = clock();
double parallel_time = (double)(end - start) / CLOCKS_PER_SEC;
printf("멀티스레드: %.3f초\n", parallel_time);
printf("정렬 검증: %s\n\n", is_sorted(arr2, n) ? "OK" : "FAIL");
printf("속도 향상: %.2fx\n", single_time / parallel_time);
free(arr1);
free(arr2);
return 0;
}
연습 문제¶
연습 1: 식사하는 철학자¶
5명의 철학자가 원탁에 앉아 있고, 젓가락 5개가 있습니다. - 철학자는 생각하거나 식사합니다 - 식사하려면 양쪽 젓가락이 필요합니다 - 데드락 없이 구현하세요
연습 2: 장벽 (Barrier)¶
N개의 스레드가 모두 도착할 때까지 대기하는 장벽을 구현하세요.
typedef struct {
int count;
int threshold;
pthread_mutex_t mutex;
pthread_cond_t cond;
} Barrier;
void barrier_wait(Barrier* b);
연습 3: 세마포어 구현¶
뮤텍스와 조건 변수를 사용하여 카운팅 세마포어를 구현하세요.
typedef struct {
int value;
pthread_mutex_t mutex;
pthread_cond_t cond;
} Semaphore;
void sem_wait(Semaphore* sem);
void sem_post(Semaphore* sem);
연습 4: 병렬 행렬 곱셈¶
N×N 행렬 곱셈을 여러 스레드로 나누어 계산하세요.
핵심 개념 정리¶
| 함수 | 설명 |
|---|---|
pthread_create() |
스레드 생성 |
pthread_join() |
스레드 종료 대기 |
pthread_mutex_lock() |
뮤텍스 잠금 |
pthread_mutex_unlock() |
뮤텍스 해제 |
pthread_cond_wait() |
조건 대기 |
pthread_cond_signal() |
하나의 대기자 깨움 |
pthread_cond_broadcast() |
모든 대기자 깨움 |
| 개념 | 설명 |
|---|---|
| 경쟁 조건 | 여러 스레드의 동시 접근으로 인한 버그 |
| 뮤텍스 | 상호 배제 (한 번에 하나만 접근) |
| 조건 변수 | 조건 만족까지 대기 |
| 데드락 | 서로 상대방의 자원을 기다리며 멈춤 |
| 생산자-소비자 | 데이터 생성/처리 분리 패턴 |
| 스레드 풀 | 미리 생성된 스레드로 작업 처리 |
디버깅 팁¶
1. ThreadSanitizer 사용¶
gcc -fsanitize=thread -g program.c -o program -lpthread
./program
2. Helgrind (Valgrind)¶
valgrind --tool=helgrind ./program
3. 일반적인 실수¶
- 뮤텍스 해제 잊음 → 모든 경로에서
unlock확인 - 조건 변수에서
while대신if사용 → 항상while사용 - 잠금 순서 불일치 → 항상 같은 순서로 잠금
C 언어 프로젝트 완료!¶
이 12개의 프로젝트를 완료했다면 C 언어의 핵심 개념을 모두 경험한 것입니다:
- ✅ 환경 설정
- ✅ C 기초 빠른 복습
- ✅ 계산기 (함수, 조건문)
- ✅ 숫자 맞추기 (반복문, 랜덤)
- ✅ 주소록 (구조체, 파일 I/O)
- ✅ 동적 배열 (메모리 관리)
- ✅ 연결 리스트 (포인터 심화)
- ✅ 파일 암호화 (비트 연산)
- ✅ 스택과 큐 (자료구조)
- ✅ 해시 테이블 (해시 함수)
- ✅ 뱀 게임 (터미널 제어)
- ✅ 미니 쉘 (프로세스 관리)
- ✅ 멀티스레드 (동시성)
다음 학습 추천: - 네트워크 프로그래밍 (소켓) - 시스템 콜 심화 - 리눅스 커널 모듈 - 임베디드 시스템