다중 스레드 구현

하스코딩·2025년 4월 12일

1. 환경설정

  1. 이번 프로젝트는 D드라이브에서 작성할 것이므로 cmd를 관리자 권한으로 열어 D에서 Ubuntu를 실행해준다.
    그리고 앞으로 사용할 Ubuntu 프로젝트 폴더를 생성하고, 해당 경로에서 WSL 기반의 VSCode 프로젝트를 실행한다.
  2. 아래 두 파일을 .vscode 폴더를 생성해 만들어준다.
  • launch.json
{
    "version": "0.2.0",
    "configurations": [
      {
        "name": "Debug problem1",
        "type": "cppdbg",
        "request": "launch",
        "program": "${workspaceFolder}/problem1.exe",
        "args": ["10", "20", "30", "40", "50"],
        "stopAtEntry": false,
        "cwd": "${workspaceFolder}",
        "environment": [],
        "externalConsole": true,
        "MIMode": "gdb",
        "miDebuggerPath": "gdb",
        "setupCommands": [
          {
            "description": "Enable pretty-printing for gdb",
            "text": "-enable-pretty-printing",
            "ignoreFailures": true
          }
        ]
      }
    ]
  }
  
  • task.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "build problem",
      "type": "shell",
      "command": "gcc",
      "args": [
        "-pthread",
        "-g",
        "problem.c",
        "-o",
        "problem"
      ],
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "problemMatcher": []
    }
  ]
}

2. 스레드 관련 함수 정리

1. pthread_create - 스레드 생성

  • 아래 프로토타입을 보면 pthread_create는 4개의 인자를 요구한다.
int pthread_create(pthread_t *thread, 
				   const pthread_attr_t *attr, 
                   void *(*start_routine) (void *), 
                   void *arg);
  1. pthread_t *thread //생성된 스레드의 ID를 저장할 변수
  2. const pthread_attr_t *attr //스레드 속성 (NULL이면 기본 속성 사용)
  3. void (start_routine)(void *) //새 스레드에서 실행할 함수의 포인터
  4. void *arg //스레드 함수에 전달할 인자 (포인터로 전달)
  • ex) &value를 넘겨주면 반드시 void arg로 받아서 ((int*)arg)로 사용해야 한다.
pthread_t tid;
int value = 10;
pthread_create(&tid, NULL, my_thread_func, &value);

2. pthread_exit - 스레드 종료

  • 현재 스레드를 종료하고 다른 스레드에 값을 반환할 수 있으나, 보통 NULL을 넘겨 종료만 하는 경우가 많다.
void pthread_exit(void *retval);

ex)

pthread_exit(NULL);  // 스레드 정상 종료

3. pthread_join — 스레드 종료 대기 (동기화)

  • 2개의 인자를 받는다.
int pthread_join(pthread_t thread, void **retval);
  1. thread 기다릴 대상 스레드의 ID
  2. retval 스레드에서 반환된 값을 저장할 포인터 (보통 NULL)
  • ex) main()함수가 종료되기 전에, 생성한 모든 스레드에 대해 pthread_join()해야 한다.
    안그러면 main이 먼저 종료되면서 스레드들도 강제종료 될 수 있다.
pthread_t tid;
pthread_create(&tid, NULL, some_function, NULL);

// main이 이 스레드가 끝날 때까지 기다림
pthread_join(tid, NULL);

4. (선택) pthreadattr* - 스레드 속성 설정

  • 보통 attr 인자는 NULL로 많이 쓰지만, 속성을 바꾸고 싶다면 pthread_attr_t를 초기화해서 사용함.
    ex) detach 상태로 만들기
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, func, NULL);
pthread_attr_destroy(&attr);

3. 문제1. 통계값 출력

  • 입력된 숫자 리스트에 대해 여러 가지 통계값을 출력하는 다중 스레드 프로그램을 작성하시오.
    이 프로그램은 명령줄 인수로 여러 개의 숫자를 전달받으며, 3개의 작업 스레드를 생성한다.
  1. 첫 번째 스레드는 숫자들의 평균,
  2. 두 번째 스레드는 숫자들의 최대값,
  3. 세 번째 스레드는 숫자들의 최솟값을 계산한다.
  • 평균, 최솟값, 최대값 저장은 전역변수를 사용, 작업 스레드는 결과를 이들 전역변수에 저장한다.
  • 모든 작업 스레드가 종료하면 부모 스레드는 이들 통계값을 출력한다.
#include <stdio.h>      // 표준 입출력 함수 사용을 위한 헤더 (printf 등)
#include <stdlib.h>     // 일반 유틸 함수 사용을 위한 헤더 (malloc, atoi 등)
#include <pthread.h>    // POSIX 스레드(pthread) 관련 함수 사용을 위한 헤더

//통계값을 저장할 전역 변수
double avg = 0.0;// 평균값
int min = 0;// 최소값
int max = 0;// 최대값

int *arr;// 입력 정수 배열 포인터 동적 할당
int len = 0;// 입력된 숫자의 총 개수

//1. 평균 계산 스레드 함수
void* calc_avg(void* arg) {
    int sum = 0;
    // 모든 숫자 합
    for (int i = 0; i < len; i++) {
        sum += arr[i];
    }
    avg = (double)sum / len;// 평균 계산 후 전역 변수에 대입
    pthread_exit(NULL);// 스레드 종료
}

//2. 최대값 계산 스레드 함수
void* calc_max(void* arg) {
    max = arr[0];
    for (int i = 1; i < len; i++) {
        if (max < arr[i]) {
            max = arr[i];// 더 큰 값 발견 시 갱신
        }
    }
    pthread_exit(NULL);// 스레드 종료
}

//3. 최소값 계산용 스레드 함수
void* calc_min(void* arg) {
    min = arr[0];
    for (int i = 1; i < len; i++) {
        if (min > arr[i]) {
            min = arr[i];// 더 작은 값 발견 시 갱신
        }
    }
    pthread_exit(NULL);// 스레드 종료
}


int main(int argc, char* argv[]) {

    //입력 개수 계산 (프로그램 이름 제외)
    len = argc - 1;

    // ▶ 입력 숫자 저장을 위한 동적 메모리 할당
    arr = (int*)malloc(len * sizeof(int));

    //명령행 인수(문자열) -> 정수로 변환하여 배열에 저장
    for (int i = 0; i < len; i++) {
        arr[i] = atoi(argv[i + 1]);
    }

    //1. 세 개의 스레드 ID 선언
    pthread_t tid_avg, tid_max, tid_min;

    //2. 3개의 스레드 생성, 함수를 넣어준다.
    pthread_create(&tid_avg, NULL, calc_avg, NULL);
    pthread_create(&tid_max, NULL, calc_max, NULL);
    pthread_create(&tid_min, NULL, calc_min, NULL);

    //3. 스레드 종료 대기 (모든 작업 스레드가 끝날 때까지 대기)
    pthread_join(tid_avg, NULL);
    pthread_join(tid_max, NULL);
    pthread_join(tid_min, NULL);

    //4. 결과 출력
    printf("Average: %.2f\n", avg);
    printf("Maximum: %d\n", max);
    printf("Minimum: %d\n", min);

    //동적 할당 해제
    free(arr);

    return 0;
}
  • 컴파일 후 실행하면서 통계값을 낼 입력 데이터를 입력하니, 평균, 최대값, 최소값 순서로 출력되었다.

4. 문제2. 소수 출력

  • 소수를 출력하는 프로그램을 작성하시오.
  1. 프로그램을 실행시키면 사용자로부터 숫자 하나를 입력받는다.
  2. 프로그램을 별도의 스레드를 만들어서 이 스레드에서 사용자가 입력한 숫자보다 작은 모든 소수를 출력한다.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdbool.h>

//1. 소수를 판별해 ture/false 리턴하는 스레드 함수
bool isPrime(int num) {
    if (num < 2) return false;// 2 미만의 수는 소수가 아님
    //2~sqrt(num)까지 반복하며 나누다, 나눠떨어지면 소수 X(false)
    for (int i = 2; i * i <= num; ++i) {
        if (num % i == 0)
            return false;
    }
    return true;// 나누어 떨어지지 않으면 소수(true)
}

//2. 소수를 출력 스레드 함수(n의 주소를 전달받음)
void* print_primes(void* n) {

    //파라미터 arg를 (int*)로 타입캐스팅 후, *연산자로 값을 출력
    int end = *((int*)n);

    printf("입력한 수 %d 보다 작은 소수들:\n", end);

    //2~end-1까지 반복하다가 소수면 출력한다.
    for (int i = 2; i < end; ++i) {
        if (isPrime(i)) {
            printf("%d ", i);
        }
    }
    printf("\n");

    pthread_exit(NULL);// 스레드 종료
}

// ▶ 메인 함수
int main() {
    int n;

    // 사용자로부터 정수 입력 받기
    printf("숫자를 입력하세요: ");
    scanf("%d", &n);

    // 입력값이 2 이하일 경우 오류 메시지 출력 후 종료
    if (n <= 2) {
        printf("2보다 큰 수를 입력해야 합니다.\n");
        return 1;
    }

    pthread_t prime_tid;//1. 소수 출력 tid

    //2. 스레드 생성: print_primes 함수를 실행하고, n의 주소 전달(call-by-reference)
    pthread_create(&prime_tid, NULL, print_primes, &n);

    //3. 메인 스레드는 prime_thread가 끝날 때까지 대기
    pthread_join(prime_tid, NULL);

    return 0;  // 프로그램 정상 종료
}
  • scanf를 사용해서 사용자에게 정수를 입력받으니, 그보다 작은 소수들을 잘 출력해주었다.

5. 문제3. 다중 스레드 정렬

  • 다음과 같이 동작하는 다중 스레드 프로그램을 작성하시오.
  1. 정수들이 들어있는 배열을 크기가 같은 2개의 부분 배열로 나눈다.
  2. 두 개의 작업 스레드가 각 부분 배열을 서로 다른 알고리즘을 이용하여 정렬한다. - 버블정렬, 선택정렬로 구현했다.
  3. 부분 배열 정렬이 끝나면 통합(merging) 스레드가 정렬된 부분 배열을 결합하여 정렬된 전체 리스트를 만든다.
  4. 숫자들을 저장하기 위해 전역 배열을 사용하고, 정렬된 숫자들을 저장하기 위해서 역시 같은 크기의 전역 배열을 사용한다.
  5. 정렬 스레드를 생성할 때 해당 스레드가 정렬해야할 배열의 범위를 인수로 전달하도록 작성하시오.
#include <stdio.h>      // 표준 입출력 함수 사용 (printf, scanf 등)
#include <stdlib.h>     // 메모리 할당, 오류 처리 등에 필요한 함수 사용 (malloc, perror 등)
#include <pthread.h>    // POSIX 스레드를 사용하기 위한 헤더

//전역 변수 선언
int* arr;// 입력 정수 배열
int* sorted;// 정렬 결과 저장 배열
int n;// 입력받은 정수 개수

//부분 배열의 구간(start~end)을 멤버변수로 가지는 구조체
typedef struct {
    int start;
    int end;
} Range;


//1. 버블 정렬 스레드 함수
void* bubbleSort_thread(void* arg) {
    Range* range = (Range*)arg;// 전달받은 인자를 Range 포인터로 변환

    for (int i = range->start; i < range->end - 1; i++) {
        for (int j = range->start+1; j < range->end - i; j++) {
            //이전원소가 더 크면 swap
            if (arr[j-1] > arr[j]) {
                int temp = arr[j];
                arr[j] = arr[j - 1];
                arr[j - 1] = temp;
            }
        }
    }
    pthread_exit(NULL);// 스레드 종료
}

//2. 선택 정렬 스레드 함수
void* selectionSort_thread(void* arg) {
    Range* range = (Range*)arg;

    for (int i = range->start; i < range->end; i++) {

        int min_idx = i;//현재위치를 최소값 인덱스로 지정
        //i+1~end를 탐색하며
        for (int j = i + 1; j < range->end; j++) {
            //최소값 인덱스 갱신
            if (arr[min_idx] > arr[j]) {
                min_idx = j; 
            }
        }

        //찾은 최소값 인덱스와 현재 위치 교환
        if(min_idx != i){
            int temp = arr[i];
            arr[i] = arr[min_idx];
            arr[min_idx] = temp;
        }
        
    }
    
    pthread_exit(NULL);
}

//3. 정렬된 두 부분 배열을 병합하는 스레드 함수
void* merge_thread(void* arg) {
    int i = 0;              // 첫 번째 절반 배열의 시작 인덱스
    int j = n / 2;       // 두 번째 절반 배열의 시작 인덱스
    int k = 0;              // 병합 결과 배열(sorted)의 인덱스

    // 두 배열을 정렬된 상태로 병합
    while (i < n / 2 && j < n) {
        if (arr[i] <= arr[j]) {
            sorted[k++] = arr[i++];
        } else {
            sorted[k++] = arr[j++];
        }
    }

    // 남아 있는 요소들 복사 (첫 번째 절반)
    while (i < n / 2) {
        sorted[k++] = arr[i++];
    }

    // 남아 있는 요소들 복사 (두 번째 절반)
    while (j < n) {
        sorted[k++] = arr[j++];
    }

    pthread_exit(NULL);  // 병합 완료 후 스레드 종료
}

int main() {
    // 사용자에게 정수 개수 입력 받음음
    printf("정수 개수 입력: ");
    scanf("%d", &n);

    // 입력 배열 및 결과 배열 동적 메모리 할당
    arr = (int*)malloc(sizeof(int) * n);
    sorted = (int*)malloc(sizeof(int) * n);

    // 정수 값들을 사용자로부터 입력 받기
    printf("정수 %d개를 입력: ", n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr[i]);
    }

    //1. 3개의 tid 변수 선언
    pthread_t tid1, tid2, tid3;

    //2. 배열을 두 부분으로 나눔
    Range first = {0, n / 2};// 앞 절반
    Range second = {n / 2, n};// 뒤 절반

    //3. 두 개의 정렬 스레드 생성
    //앞쪽 절반 -> 버블정렬, 뒤쪽 절반 -> 선택정렬
    pthread_create(&tid1, NULL, bubbleSort_thread, &first);
    pthread_create(&tid2, NULL, selectionSort_thread, &second);
    pthread_join(tid1, NULL);//정렬이 끝날 때까지 대기기
    pthread_join(tid2, NULL);


    //4. 병합 스레드 생성 및 실행
    pthread_create(&tid3, NULL, merge_thread, NULL);
    pthread_join(tid3, NULL);// 병합이 끝날 때까지 대기


    // 최종 정렬된 배열 출력
    printf("정렬 결과: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", sorted[i]);
    }
    printf("\n");

    // 동적 할당 해제
    free(arr);
    free(sorted);

    return 0;
}
  • 입력할 정수의 개수를 먼저 입력받고, n개의 정수를 입력받는다.
  • 각각 절반씩 나눠 정렬 후 merge 해서 정렬된 결과를 출력했다.

0개의 댓글