C 언어에서 스레드 풀(Thread Pool) 구현하기: 기본부터 고급까지

스레드 풀(Thread Pool)은 멀티스레드 프로그래밍에서 성능을 극대화하고 리소스 사용을 최적화하기 위한 중요한 기법입니다. 프로그램이 처리해야 할 작업이 많아질 때, 스레드를 반복적으로 생성하고 삭제하는 대신, 미리 생성된 스레드들의 풀을 사용하여 작업을 처리할 수 있습니다. 본 기사에서는 C 언어를 사용하여 스레드 풀을 직접 구현하고, 이를 통해 효율적인 멀티스레드 프로그래밍을 실현하는 방법을 설명합니다.

목차

스레드 풀의 개념과 중요성


스레드 풀(Thread Pool)은 미리 생성된 스레드 그룹으로, 작업이 요청되면 스레드를 재사용하여 작업을 처리하는 구조입니다. 이는 스레드 생성과 소멸의 오버헤드를 줄여 성능을 최적화하고, 시스템 자원의 과도한 사용을 방지합니다.

스레드 풀의 기본 개념


스레드 풀은 다음과 같은 방식으로 동작합니다:

  1. 프로그램 시작 시 일정 수의 스레드를 생성해 풀에 저장.
  2. 작업 요청 시, 풀에서 유휴 상태의 스레드가 작업을 처리.
  3. 작업이 끝나면 스레드는 유휴 상태로 돌아가 풀에 남음.

스레드 풀이 중요한 이유

  • 성능 향상: 스레드를 반복적으로 생성 및 삭제하는 비용을 절감.
  • 자원 관리: 시스템이 처리할 수 있는 스레드 수를 제한하여 과도한 리소스 사용 방지.
  • 응답 속도 개선: 작업 요청 시 스레드를 즉시 사용할 수 있어 응답 속도가 빨라짐.

스레드 풀이 사용되는 사례

  • 웹 서버: 다수의 클라이언트 요청을 병렬로 처리.
  • 파일 처리: 대규모 파일 I/O 작업을 효율적으로 분산 처리.
  • 데이터베이스: 다중 쿼리를 동시에 처리하여 성능 향상.

스레드 풀은 복잡한 멀티스레드 환경에서 간결하면서도 강력한 자원 관리 방법을 제공합니다. C 언어에서의 구현을 통해 이를 직접 체험해볼 수 있습니다.

C 언어에서 스레드 풀 구현을 위한 사전 준비

스레드 풀을 구현하기 위해서는 멀티스레드 환경을 지원하는 적절한 도구와 라이브러리를 사용하는 것이 중요합니다. C 언어로 구현할 때 필수적으로 설정해야 하는 환경과 준비 요소를 아래에 정리했습니다.

필요한 라이브러리

  • POSIX 스레드(Pthreads): 대부분의 유닉스 기반 시스템에서 제공되는 표준 멀티스레드 라이브러리입니다.
  • 헤더 파일: <pthread.h>
  • 주요 함수: pthread_create, pthread_join, pthread_mutex_lock, pthread_cond_wait 등.

개발 환경 설정

  1. 컴파일러
  • GCC(g++)와 같은 컴파일러에서 POSIX 스레드 지원 옵션을 활성화해야 합니다.
  • 예: gcc -pthread thread_pool.c -o thread_pool
  1. 플랫폼 확인
  • POSIX 표준을 준수하지 않는 플랫폼(예: Windows)에서는 Windows API를 사용하거나, 호환성을 고려한 라이브러리 선택 필요.

코드 작성 준비

  • 헤더 파일 정의
  • 스레드 풀 구현에 필요한 데이터 구조와 함수 정의를 위한 헤더 파일 작성.
  • 큐 데이터 구조
  • 작업을 저장하고 스레드에서 처리하기 위한 큐를 설계.
  • 동기화 도구 설정
  • 작업의 순차적 처리를 보장하기 위한 뮤텍스 및 조건 변수 설정.

테스트 환경 준비


스레드 풀 구현 후 이를 테스트할 수 있는 간단한 프로그램 작성. 예를 들어, 작업 큐에 입력된 숫자의 제곱을 계산하는 작업으로 스레드 풀이 제대로 작동하는지 확인합니다.

사전 준비를 통해 스레드 풀 구현을 위한 기초를 다질 수 있으며, 이를 바탕으로 안정적이고 효율적인 코드 작성이 가능합니다.

스레드 풀의 기본 구조 설계

스레드 풀의 구현은 효율적인 설계를 통해 이루어집니다. 기본 구조 설계 단계에서는 스레드 풀을 구성하는 주요 컴포넌트를 정의하고, 각 컴포넌트의 역할과 상호작용을 명확히 하는 것이 중요합니다.

스레드 풀의 주요 컴포넌트

  1. 작업 큐 (Task Queue)
  • 스레드가 처리해야 할 작업들을 저장.
  • FIFO(First In, First Out) 방식으로 동작하며, 작업 추가와 제거를 위한 동기화 메커니즘이 필요.
  • 큐의 크기를 제한하여 과도한 작업이 추가되는 것을 방지.
  1. 스레드 풀 관리자 (Thread Pool Manager)
  • 스레드를 초기화하고, 작업 큐에서 작업을 가져와 실행.
  • 실행 가능한 스레드의 수를 제한하고, 유휴 상태 관리.
  1. 스레드 (Threads)
  • 스레드 풀에 미리 생성된 작업 처리 단위.
  • 각 스레드는 작업 큐에서 작업을 가져와 실행하며, 완료 후 대기 상태로 전환.

스레드 풀의 구조 설계

  • 데이터 구조
  • 작업 큐를 구현하기 위한 자료 구조(링크드 리스트 또는 배열).
  • 스레드 풀의 스레드와 작업 큐를 관리하기 위한 구조체 정의.
typedef struct {
    void (*function)(void*);  // 작업 함수
    void *argument;           // 작업 함수 인자
} Task;

typedef struct {
    Task *tasks;              // 작업 큐
    int queue_size;           // 큐 크기
    int front;                // 큐의 시작 위치
    int rear;                 // 큐의 끝 위치
    pthread_mutex_t lock;     // 큐 접근 동기화
    pthread_cond_t notify;    // 작업 알림
} TaskQueue;
  • 스레드 풀 구조체
  • 작업 큐와 스레드 배열, 스레드 풀의 상태를 포함.
typedef struct {
    pthread_t *threads;       // 스레드 배열
    TaskQueue task_queue;     // 작업 큐
    int thread_count;         // 스레드 수
    int shutdown;             // 종료 플래그
} ThreadPool;

설계 시 고려사항

  1. 동기화
  • 작업 큐 접근 시 동기화를 통해 경쟁 조건을 방지.
  • 뮤텍스와 조건 변수를 사용하여 작업이 추가되거나 제거될 때 신호를 전달.
  1. 확장성
  • 초기 스레드 수와 큐 크기를 설정하되, 필요 시 유동적으로 확장 가능하게 설계.
  1. 에러 처리
  • 작업 추가 및 스레드 생성 시 실패 처리 방안 마련.
  • 스레드 풀 종료 시 모든 작업이 완료되었는지 확인.

기본 구조 설계를 통해 스레드 풀의 효율성과 안정성을 확보할 수 있으며, 이후 구현 단계에서 이 설계를 기반으로 실제 코드를 작성하게 됩니다.

작업 큐의 구현

작업 큐(Task Queue)는 스레드 풀이 처리할 작업을 관리하는 핵심 컴포넌트입니다. 작업 큐는 작업이 추가되고 스레드가 이를 처리할 수 있도록 FIFO(First In, First Out) 방식으로 동작해야 합니다.

작업 큐의 기본 원리

  • 작업 큐는 생산자-소비자 패턴을 따릅니다.
  • 생산자: 작업을 큐에 추가.
  • 소비자: 큐에서 작업을 가져와 처리.
  • 동기화 메커니즘을 통해 다중 스레드 환경에서 데이터 경합을 방지.

작업 큐 구조 설계


작업 큐는 작업을 저장하기 위한 배열과 작업 추가 및 제거를 위한 인덱스를 포함하는 데이터 구조로 설계됩니다.

typedef struct {
    void (*function)(void*);  // 작업 함수
    void *argument;           // 작업 함수의 인자
} Task;

typedef struct {
    Task *tasks;              // 작업 배열
    int capacity;             // 작업 큐 용량
    int size;                 // 현재 작업 수
    int front;                // 큐의 앞쪽 인덱스
    int rear;                 // 큐의 뒤쪽 인덱스
    pthread_mutex_t lock;     // 큐 접근 동기화
    pthread_cond_t not_empty; // 큐가 비어 있지 않음을 알리는 조건 변수
    pthread_cond_t not_full;  // 큐가 가득 차지 않았음을 알리는 조건 변수
} TaskQueue;

작업 큐의 주요 기능

1. 작업 추가 (Enqueue)


작업을 큐에 추가하는 함수는 큐가 가득 차지 않았는지 확인하고, 작업을 배열에 추가한 후 뒤쪽 인덱스를 업데이트합니다.

int enqueue(TaskQueue *queue, void (*function)(void *), void *argument) {
    pthread_mutex_lock(&queue->lock);

    while (queue->size == queue->capacity) {
        pthread_cond_wait(&queue->not_full, &queue->lock);
    }

    queue->tasks[queue->rear].function = function;
    queue->tasks[queue->rear].argument = argument;
    queue->rear = (queue->rear + 1) % queue->capacity;
    queue->size++;

    pthread_cond_signal(&queue->not_empty);
    pthread_mutex_unlock(&queue->lock);
    return 0;
}

2. 작업 제거 (Dequeue)


작업을 큐에서 제거하는 함수는 큐가 비어 있지 않은지 확인하고, 앞쪽 인덱스에서 작업을 가져온 후 큐 크기를 업데이트합니다.

int dequeue(TaskQueue *queue, Task *task) {
    pthread_mutex_lock(&queue->lock);

    while (queue->size == 0) {
        pthread_cond_wait(&queue->not_empty, &queue->lock);
    }

    *task = queue->tasks[queue->front];
    queue->front = (queue->front + 1) % queue->capacity;
    queue->size--;

    pthread_cond_signal(&queue->not_full);
    pthread_mutex_unlock(&queue->lock);
    return 0;
}

초기화 및 종료 함수

  • 초기화: 작업 큐의 메모리를 할당하고 뮤텍스 및 조건 변수를 초기화.
  • 종료: 작업 큐의 메모리를 해제하고 동기화 객체를 파괴.
void init_task_queue(TaskQueue *queue, int capacity) {
    queue->tasks = malloc(sizeof(Task) * capacity);
    queue->capacity = capacity;
    queue->size = 0;
    queue->front = 0;
    queue->rear = 0;
    pthread_mutex_init(&queue->lock, NULL);
    pthread_cond_init(&queue->not_empty, NULL);
    pthread_cond_init(&queue->not_full, NULL);
}

void destroy_task_queue(TaskQueue *queue) {
    free(queue->tasks);
    pthread_mutex_destroy(&queue->lock);
    pthread_cond_destroy(&queue->not_empty);
    pthread_cond_destroy(&queue->not_full);
}

작업 큐 설계의 이점

  • 효율성: FIFO 방식으로 작업을 처리하여 작업 흐름의 순서를 보장.
  • 동시성: 동기화를 통해 다중 스레드 환경에서도 안전하게 동작.
  • 유연성: 작업 큐의 크기를 조정하여 다양한 요구 사항에 대응.

이 작업 큐는 스레드 풀의 핵심 역할을 담당하며, 스레드가 효율적으로 작업을 처리할 수 있도록 지원합니다.

스레드 관리와 풀 초기화

스레드 풀 초기화 단계에서는 스레드 생성 및 관리를 통해 스레드 풀이 작업을 처리할 준비를 합니다. 이 과정은 스레드 풀의 동작을 안정적이고 효율적으로 유지하는 데 핵심적입니다.

스레드 풀 초기화


스레드 풀의 초기화는 풀에 사용할 스레드와 작업 큐를 설정하고 동기화를 위한 구조를 준비하는 단계로 구성됩니다.

스레드 풀 구조체 정의

typedef struct {
    pthread_t *threads;       // 스레드 배열
    TaskQueue task_queue;     // 작업 큐
    int thread_count;         // 스레드 수
    int shutdown;             // 스레드 풀 종료 여부
    pthread_mutex_t lock;     // 스레드 풀 동기화
    pthread_cond_t notify;    // 스레드 상태 알림
} ThreadPool;

초기화 함수


스레드 풀을 초기화하여 작업을 처리할 준비를 합니다.

ThreadPool* init_thread_pool(int thread_count, int queue_capacity) {
    ThreadPool *pool = malloc(sizeof(ThreadPool));
    if (!pool) return NULL;

    pool->thread_count = thread_count;
    pool->shutdown = 0;
    pool->threads = malloc(sizeof(pthread_t) * thread_count);
    if (!pool->threads) {
        free(pool);
        return NULL;
    }

    init_task_queue(&pool->task_queue, queue_capacity);
    pthread_mutex_init(&pool->lock, NULL);
    pthread_cond_init(&pool->notify, NULL);

    for (int i = 0; i < thread_count; i++) {
        pthread_create(&pool->threads[i], NULL, thread_worker, pool);
    }

    return pool;
}

스레드 작업 함수


각 스레드는 작업 큐에서 작업을 가져와 실행하고, 작업이 없으면 대기 상태로 전환합니다.

void* thread_worker(void *arg) {
    ThreadPool *pool = (ThreadPool *)arg;
    Task task;

    while (1) {
        pthread_mutex_lock(&pool->lock);

        while (pool->task_queue.size == 0 && !pool->shutdown) {
            pthread_cond_wait(&pool->notify, &pool->lock);
        }

        if (pool->shutdown) {
            pthread_mutex_unlock(&pool->lock);
            pthread_exit(NULL);
        }

        dequeue(&pool->task_queue, &task);
        pthread_mutex_unlock(&pool->lock);

        (*(task.function))(task.argument);
    }

    return NULL;
}

스레드 종료 및 자원 정리


스레드 풀이 종료될 때 모든 스레드가 안전하게 종료되도록 하고, 동기화 객체와 작업 큐를 정리합니다.

void destroy_thread_pool(ThreadPool *pool) {
    pthread_mutex_lock(&pool->lock);
    pool->shutdown = 1;
    pthread_cond_broadcast(&pool->notify);
    pthread_mutex_unlock(&pool->lock);

    for (int i = 0; i < pool->thread_count; i++) {
        pthread_join(pool->threads[i], NULL);
    }

    destroy_task_queue(&pool->task_queue);
    pthread_mutex_destroy(&pool->lock);
    pthread_cond_destroy(&pool->notify);
    free(pool->threads);
    free(pool);
}

설계 시 고려사항

  1. 스레드 안전성: 작업 큐와 스레드 풀의 상태를 보호하기 위한 동기화 메커니즘 필수.
  2. 유연한 확장성: 다양한 작업 부하에 적응할 수 있도록 스레드와 큐 크기를 조정 가능.
  3. 자원 누수 방지: 스레드 종료와 메모리 해제를 확실히 처리.

초기화의 중요성


스레드 풀이 제대로 초기화되지 않으면 작업 처리에 병목이 생기거나 충돌 문제가 발생할 수 있습니다. 따라서 안정적인 초기화는 스레드 풀의 성능과 신뢰성을 보장하는 중요한 단계입니다.

동기화 메커니즘과 작업 처리

스레드 풀에서 동기화 메커니즘은 다중 스레드 환경에서 데이터의 무결성을 보장하고, 작업 처리의 효율성을 유지하는 핵심 요소입니다. C 언어에서는 뮤텍스와 조건 변수를 사용해 이러한 동기화를 구현할 수 있습니다.

동기화 메커니즘

1. 뮤텍스 (Mutex)

  • 역할: 공유 자원(예: 작업 큐)에 대한 동시 접근을 방지.
  • 사용 방법:
  • 공유 자원에 접근하기 전에 pthread_mutex_lock으로 잠금.
  • 자원 사용 후 pthread_mutex_unlock으로 잠금 해제.

2. 조건 변수 (Condition Variable)

  • 역할: 특정 조건이 충족될 때까지 스레드를 대기 상태로 유지.
  • 사용 방법:
  • 조건을 기다릴 때 pthread_cond_wait 사용.
  • 조건이 충족되었을 때 pthread_cond_signal 또는 pthread_cond_broadcast로 대기 중인 스레드에 알림.

동기화 예제: 작업 큐 동기화

void enqueue_task(ThreadPool *pool, void (*function)(void *), void *argument) {
    pthread_mutex_lock(&pool->task_queue.lock);

    while (pool->task_queue.size == pool->task_queue.capacity) {
        pthread_cond_wait(&pool->task_queue.not_full, &pool->task_queue.lock);
    }

    enqueue(&pool->task_queue, function, argument);

    pthread_cond_signal(&pool->task_queue.not_empty);
    pthread_mutex_unlock(&pool->task_queue.lock);
}

void dequeue_task(ThreadPool *pool, Task *task) {
    pthread_mutex_lock(&pool->task_queue.lock);

    while (pool->task_queue.size == 0 && !pool->shutdown) {
        pthread_cond_wait(&pool->task_queue.not_empty, &pool->task_queue.lock);
    }

    if (pool->task_queue.size > 0) {
        dequeue(&pool->task_queue, task);
    }

    pthread_cond_signal(&pool->task_queue.not_full);
    pthread_mutex_unlock(&pool->task_queue.lock);
}

작업 처리 흐름

  1. 작업 추가
  • 생산자(메인 프로그램 또는 작업 생성자)가 새로운 작업을 큐에 추가.
  • 작업 추가 시 큐의 크기와 상태를 동기화로 관리.
  1. 작업 소비
  • 스레드가 작업 큐에서 작업을 가져와 처리.
  • 작업 처리 후 작업 큐의 상태를 업데이트하고 다른 스레드가 접근할 수 있도록 신호를 전달.
  1. 대기 상태 관리
  • 작업이 없는 경우 스레드는 pthread_cond_wait로 대기.
  • 작업이 추가되면 pthread_cond_signal로 대기 중인 스레드를 깨움.

효율적 동기화 설계의 중요성

  • 경쟁 조건 방지: 다수의 스레드가 동시에 작업 큐에 접근하더라도 데이터 손상이 발생하지 않음.
  • 공정성: 작업 처리 순서가 FIFO 방식으로 유지됨.
  • 성능 최적화: 최소한의 잠금으로 동기화를 구현하여 스레드가 과도하게 대기하지 않도록 함.

예제: 동기화된 작업 처리

void example_function(void *arg) {
    int *num = (int *)arg;
    printf("Processing task: %d\n", *num);
    free(num);
}

int main() {
    ThreadPool *pool = init_thread_pool(4, 10);

    for (int i = 0; i < 20; i++) {
        int *task_arg = malloc(sizeof(int));
        *task_arg = i;
        enqueue_task(pool, example_function, task_arg);
    }

    destroy_thread_pool(pool);
    return 0;
}

결론


동기화 메커니즘은 스레드 풀이 안정적으로 동작하기 위한 핵심입니다. 적절한 동기화 설계를 통해 스레드 간 경쟁 조건을 방지하고, 작업 처리의 효율성을 극대화할 수 있습니다.

스레드 풀 디버깅 및 최적화

스레드 풀은 멀티스레드 환경에서 발생할 수 있는 복잡한 문제를 포함하기 때문에 디버깅과 성능 최적화가 필수적입니다. 올바른 디버깅 방법과 최적화 전략을 적용하면 스레드 풀의 안정성과 효율성을 향상시킬 수 있습니다.

스레드 풀 디버깅

1. 일반적인 문제

  • 데드락(Deadlock): 스레드가 서로 다른 리소스를 잠그고 대기하면서 무한히 멈춤.
  • 경쟁 조건(Race Condition): 여러 스레드가 동시에 공유 데이터에 접근하여 예측할 수 없는 결과를 초래.
  • 리소스 누수(Resource Leak): 스레드 종료 또는 메모리 해제가 올바르게 수행되지 않음.

2. 디버깅 방법

  1. 로그 추가
  • 작업 큐 상태, 스레드 동작, 뮤텍스 잠금/해제 여부 등을 로그로 기록.
  • 예: 작업 추가 및 제거 시 로그 출력.
printf("Task added to queue: Function=%p, Argument=%p\n", function, argument);
printf("Task removed from queue: Function=%p, Argument=%p\n", task.function, task.argument);
  1. 디버깅 툴 사용
  • Valgrind: 메모리 누수 및 경쟁 조건 확인.
  • GDB: 스레드 상태와 변수 값 확인.
  1. 상태 점검 변수 활용
  • 스레드 풀의 상태를 확인하기 위한 디버깅 플래그 또는 카운터 추가.
  • 예: 활성 스레드 수와 작업 큐의 크기 모니터링.

스레드 풀 최적화

1. 스레드 수 최적화

  • 스레드 수 계산: 시스템의 CPU 코어 수와 작업 부하를 고려하여 적절한 스레드 수를 설정.
  • 일반적으로 CPU 코어 수 + 1이 적합.
  • 동적 스레드 조정: 작업 부하에 따라 스레드 수를 동적으로 조정.

2. 작업 큐 최적화

  • 큐 크기 조정: 적절한 큐 크기를 설정하여 작업 처리 병목 현상을 최소화.
  • 큐 구현 방식 변경: 배열 기반 큐 대신 링크드 리스트로 구현하여 메모리 효율성 개선.

3. 동기화 최적화

  • 잠금 범위 최소화: 뮤텍스 잠금 범위를 줄여 동기화로 인한 병목을 방지.
  • 읽기/쓰기 분리: 읽기 작업과 쓰기 작업에 대해 별도의 뮤텍스를 사용하여 병렬 처리 성능을 개선.

예제: 최적화된 스레드 풀

void* optimized_thread_worker(void *arg) {
    ThreadPool *pool = (ThreadPool *)arg;
    Task task;

    while (1) {
        pthread_mutex_lock(&pool->lock);

        while (pool->task_queue.size == 0 && !pool->shutdown) {
            pthread_cond_wait(&pool->notify, &pool->lock);
        }

        if (pool->shutdown) {
            pthread_mutex_unlock(&pool->lock);
            pthread_exit(NULL);
        }

        dequeue(&pool->task_queue, &task);
        pthread_mutex_unlock(&pool->lock);

        (*(task.function))(task.argument); // 작업 실행

        // 작업 완료 로그
        printf("Task completed: Function=%p, Argument=%p\n", task.function, task.argument);
    }

    return NULL;
}

성능 모니터링

  • CPU 및 메모리 사용량 확인: 시스템 리소스의 과도한 사용 방지.
  • 작업 처리 시간 측정: 작업이 스레드 풀에서 처리되는 데 걸리는 시간을 기록.

결론


스레드 풀의 디버깅과 최적화를 통해 안정적이고 성능이 뛰어난 멀티스레드 프로그램을 구현할 수 있습니다. 디버깅 도구와 로그를 활용하여 문제를 파악하고, 최적화 전략으로 병목 현상을 제거하여 효율성을 극대화하세요.

응용 예시: 파일 처리 프로그램

스레드 풀을 활용한 응용 프로그램으로 대규모 파일 처리를 예로 들 수 있습니다. 이 프로그램은 여러 개의 파일에서 데이터를 읽고, 특정 작업(예: 단어 개수 세기)을 수행한 뒤 결과를 출력합니다. 스레드 풀을 사용하면 파일 처리 작업을 병렬로 수행하여 처리 속도를 크게 향상시킬 수 있습니다.

프로그램 요구사항

  1. 여러 개의 텍스트 파일에서 동시에 데이터를 처리.
  2. 각 파일의 단어 개수를 계산하여 출력.
  3. 스레드 풀을 사용해 작업을 병렬로 분산 처리.

구현 코드

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include "thread_pool.h"  // 이전 단계에서 구현한 스레드 풀 헤더 파일

// 파일 처리 작업 함수
void process_file(void *arg) {
    char *file_name = (char *)arg;
    FILE *file = fopen(file_name, "r");
    if (!file) {
        printf("Failed to open file: %s\n", file_name);
        free(file_name);
        return;
    }

    int word_count = 0;
    char buffer[1024];

    while (fscanf(file, "%1023s", buffer) == 1) {
        word_count++;
    }

    fclose(file);
    printf("File: %s, Word Count: %d\n", file_name, word_count);
    free(file_name);
}

int main() {
    // 파일 목록
    const char *files[] = {"file1.txt", "file2.txt", "file3.txt", "file4.txt"};
    int file_count = sizeof(files) / sizeof(files[0]);

    // 스레드 풀 초기화
    ThreadPool *pool = init_thread_pool(4, 10);

    // 각 파일을 처리하도록 작업 추가
    for (int i = 0; i < file_count; i++) {
        char *file_name = malloc(strlen(files[i]) + 1);
        strcpy(file_name, files[i]);
        enqueue_task(pool, process_file, file_name);
    }

    // 스레드 풀 종료 및 정리
    destroy_thread_pool(pool);

    return 0;
}

코드 설명

  1. process_file 함수
  • 파일 이름을 받아 해당 파일을 열고 데이터를 처리.
  • 단어를 하나씩 읽어 개수를 계산한 뒤 결과를 출력.
  1. enqueue_task 함수
  • 각 파일 처리 작업을 스레드 풀의 작업 큐에 추가.
  • 파일 이름은 동적으로 할당하여 각 작업에 전달.
  1. 스레드 풀 활용
  • init_thread_pool로 스레드 풀을 초기화하고, 작업을 추가하여 병렬 처리.
  • 작업 완료 후 destroy_thread_pool로 메모리와 스레드 자원 정리.

결과 예시


프로그램 실행 시 출력:

File: file1.txt, Word Count: 120
File: file2.txt, Word Count: 95
File: file3.txt, Word Count: 150
File: file4.txt, Word Count: 85

장점

  1. 병렬 처리로 성능 향상
  • 스레드 풀이 여러 파일을 동시에 처리하여 작업 시간이 단축.
  1. 자원 관리 효율화
  • 스레드 풀로 스레드 생성/소멸의 오버헤드 제거.
  1. 유지보수 용이성
  • 코드가 모듈화되어 새로운 파일 처리 작업을 쉽게 추가 가능.

결론


스레드 풀은 대규모 파일 처리와 같은 병렬 작업에 효과적입니다. 이 예시를 통해 스레드 풀을 활용한 프로그램 설계와 구현의 기본 원리를 이해하고, 실제 프로젝트에 응용할 수 있습니다.

요약

본 기사에서는 C 언어로 스레드 풀을 구현하는 방법을 기본 개념부터 고급 응용 예시까지 단계별로 설명했습니다. 스레드 풀은 작업 큐와 동기화 메커니즘을 통해 병렬 작업을 효율적으로 처리하며, 성능 최적화와 자원 관리를 돕습니다. 마지막으로, 대규모 파일 처리 프로그램을 예시로 스레드 풀의 실제 활용 사례를 소개했습니다. 이를 통해 멀티스레드 프로그래밍에서 스레드 풀의 필요성과 이점을 깊이 이해할 수 있습니다.

목차