[운영체제/OS] 멀티쓰레드 구현해보기

kindof

·

2021. 6. 14. 20:35

1. 멀티쓰레드의 타입

이전 글에서 프로세스 모델과 쓰레드 모델의 차이를 설명했습니다.

 

프로세스 모델은 각 프로세스 안에 모든 Image와 Context들을 담고 있기 때문에 무거우며, 새로운 프로세스를 만들 때마다 fork()를 수행하기 때문에 모든 내용을 복제하는 메모리 상의 비효율성과 시간적 비효율성을 초래한다고 했죠. 그리고 이러한 맥락에서 Thread를 사용하게 되면 쓰레드를 생성하는 데 시간도 적게 걸리며 요청을 병렬적으로 처리할 수 있기 때문에 효율성을 확보할 수 있다고 했습니다.

 

그렇다면 실제로 멀티쓰레딩은 어떻게 구현할까요? 이에 대해서는 크게 두 가지 방법이 있는데, 아래 그림을 보도록 하겠습니다.

 

멀티 쓰레딩의 구현

1) User-level Threading

왼쪽 그림이 User-level Thread를 표현한 것입니다.

 

그림에서 보시다시피, 커널은 프로세스 테이블에 대한 정보만을 가지고 있고, 프로세스 자체를 스케줄링의 대상으로 봅니다. 그리고 그 프로세스 안에서만 쓰레드가 개별적으로 동작하죠.

 

따라서, 한 프로세스 안에 있는 쓰레드들은 운명 공동체가 되어버리며, 만약 한 Thread에서 Block이 발생하면, 그 프로세스 전체가 Block되는 문제가 생기게 됩니다.

 

또한, 프로세스는 CPU 하나에서만 돌아가기 때문에 멀티프로세서 아키텍쳐에서 효율이 더 좋아진다는 보장이 없죠.

 

2) Kernel-level Threading

Kernel-level Threading을 나타낸 그림이 오른쪽 그림입니다. 지금 우리가 쓰는 OS는 대부분 Kernel-level Threading을 구현하는데요.

 

User-level Threading과는 다르게 각 Thread가 프로세스에 종속적이지 않고 독립적으로 스케줄링 됩니다.

 

커널(OS)은 직접 쓰레드들에 대한 테이블을 스택 영역에서 관리하게 되며, 스케줄러가 쓰레드 각각의 실행을 관리합니다.

 

User-level Threading과는 달리 하나의 Thread에서 Block이 일어나도, 그냥 다른 Thread에게 실행권을 넘겨주면 되기 때문에 전체 프로세스가 망가질 이유가 없고, 여러 Thread가 여러 CPU에서 동시에 돌아갈 수 있게 되어 Blocking System call 이슈를 해결할 수 있죠.

 

하지만 이 방식에서는 쓰레드를 만들 때 커널 모드로 가서 쓰레드를 만들어줘야 하기 때문에 어쩔 수 없이 시스템 콜에 의한 인터럽트가 수반되며, 쓰레드의 스케줄링과 동기화 이슈를 처리해야 하기 때문에 무거워진다는 단점이 있습니다.

 

 

 

2. Pthread

Pthread는 멀티쓰레딩 기반 애플리케이션에 사용하는 C 라이브러리입니다.

 

역사적으로 거슬러 올라가보면, 멀티쓰레드를 커널에 어떻게 구현하는가에 대한 이슈에 많은 개발자들이 각각 다른 해결책과 구현법을 내놓았는데, pthread 라이브러리는 이들을 통합하여 제공하는 API입니다.


자, 그러면 이제 Pthread 함수의 기본 함수와 사용 방법에 대해서 알아보겠습니다.

 

상식적으로(?) 생각해보면 멀티쓰레딩을 하기 위해서는 우선 쓰레드를 생성, 자원에 대한 동기화 처리, 다른 쓰레드의 결과값 받아오기, 종료하기 등의 기능이 필요할 것 같습니다.

/**
* th_id: pthread 식별자로 thread가 성공적으로 생성되면 thread 식별값이 주어진다.
* attr: pthread의 속성으로 기본적인 thread 속성을 사용할 경우 Null
* 함수명: pthread로 분기할 함수를 나타낸다. 
* 함수명은 반환값이 void* 타입이고 매개변수도 void*으로 선언된 함수만 가능하다.
* arg: 분기할 함수로 넘겨줄 인자값이다. 
* 어떤 자료형을 넘겨줄 지 모르기 때문에 void형으로 넘겨주고 상황에 맞게 분기하는 함수 내에서 원래 자료형으로 캐스팅해서 사용한다.
* 리턴 값: 성공적으로 pthread가 생성될 경우 0을 반환한다.
*/
int pthread_create(pthread_t *th_id, const pthread_attr_t *attr, void *함수명, void *arg);


/**
* 특정 pthread가 종료될 때까지 기다리다가 특정 pthread가 종료 시 자원 해제시켜준다.
* 첫번째 인자는 어떤 pthread를 기다릴 지 정하는 식별자이고, 두번째 인자는 pthread의 리턴값으로 포인터로 값을 받아온다.
*/
int pthread_join(pthread_t th_id, void** thread_return);


/**
* th_id 식별자를 가지는 pthread가 부모 pthread로부터 독립한다. 
* 이렇게 독립된 pthread는 따로 pthread_join()없이도 종료 시 자동으로 리소스 해제된다.
*/
int pthread_detach(pthread_t th_id);


/**
* 현재 실행 중인 thread를 종료시킬 때 사용한다. 
* 보통 pthread_exit가 호출되면 cleanup handler가 호출되어 리소스를 해제한다.
*/
void pthread_exit(void* ret_value);


/**
* 현재 동작인 pthread 식별자를 리턴한다.
*/
pthread_t pthread_self(void);

 


위에서 살펴본 Pthread를 활용해서 간단한 예제 코드를 공부해볼까요?

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

/* 최대 쓰레드 개수 */
#define MAX_THREAD_NUM 5

/* thread main function 선언 */
void *thread_function(void *arg);

int main()
{
    int res;
    pthread_t thread_handle[MAX_THREAD_NUM];
    void *thread_result;
    int i;

    /* MAX_THREAD_NUM만큼 pthread를 생성한다.*/
    for(i = 0; i < MAX_THREAD_NUM; i++){
        /**
        pthread를 생성하여 생성된 pthread에 대한 handle을 thread_handle 배열에 저장한다.
        pthread의 main_function은 세번째 인자인 thread_function이 되며, 인자로 몇번째로 생성된 pthread인지를 나타내는
        i값을 넘긴다.
        */
        res = pthread_create(&(thread_handle[i]), NULL, thread_function, (void *)i);
        /* 결과값을 확인하여 Error가 발생하였는지 확인한다. */
        if(res != 0){
            printf("[Main] Thread Creation Failed.\n");
            exit(1);
        } 
    }

    /* 메시지 출력 */
    printf("[Main] Waiting for threads to finish..\n");

    /* MAX_THREAD_NUM만큼 생성된 모든 pthread가 종료되기를 기다린다. */
    for(i = 0; i < MAX_THREAD_NUM; i++){
        /*
        생성된 pthread가 종료되기를 기다린다.
        pthread 내부에서 pthread_exit가 호출되기 전까지 블록된다.
        pthread_exit의 인자를 pthread_result값으로 전달받는다.
        */

        res = pthread_join(thread_handle[i], &thread_result);

        /* 결과값을 확인하여 pthread가 올바로 join되었는지 확인한다. */
        if(res == 0){
            printf("[Main] join thread(%d)\n",i);
        }else{
            printf("[Main] join thread(%d) failed\n", i);
        }
    }

    /* 메시지 출력 */
    printf("[Main] All done\n");
    exit(1); 
} 

void *thread_function(void *arg)
{
    /* 인자로 받은 값을 my_number에 저장한다. */
    int my_number = *((int*)(&arg));
    int rand_num;

    /* 몇번째 pthread가 실행중인지 메시지를 출력한다. */
    printf("[thread(%d)] is running\n", my_number);

    /* 1부터 5 사이의 임의의 값을 생성한다. */
    rand_num = 1 + (int)(5.0*rand()/(RAND_MAX+1.0));

    /* 생성된 임의의 값만큼 sleep한다. */
    sleep(rand_num);

    /* sleep이 끝났음을 메시지로 출력한다 */
    printf("[thread(%d)] job done\n", my_number);

    /* thread 작업이 종료되었음을 알린다. */
    pthread_exit(NULL);
}

위 코드에 대한 설명은 대부분 주석에 적혀있습니다. 그래도 한 번 간단하게 풀어서 설명해보겠습니다.

 

자신이 가지고 있는 컴퓨터의 코어에 따라 쓰레드 개수의 한계가 있을텐데요. 우선 해당 프로그램에서 사용하게 될 쓰레드의 개수는 5개입니다. 

 

최초에 5개의 쓰레드를 생성하고 이 쓰레드들은 p_thread 타입의 thread_handle 배열에서 생성된 순서를 부여하여 관리합니다. 그리고 각 쓰레드는 thread_function()이라는 함수를 실행하게 되는데요. 여기서 몇 번째 쓰레드가 실행 중인지에 대한 메시지를 출력하여 함수 동작을 확인할 수 있습니다.

 

그리고 각 쓰레드가 함수 실행을 완료하면 종료 메시지를 출력하고 pthread_exit()을 통해 종료됩니다. 그러면 메인 쓰레드는 join()을 통해 자식 쓰레드의 종료를 기다리죠. 모든 자식 쓰레드가 종료되면 메인 쓰레드가 최종적으로 종료됩니다.

 

아래는 위 코드의 실행 결과입니다.

 


 

4.마치면서

멀티쓰레딩의 필요성에 대해 이전 글에서 공부했고 오늘은 쓰레드를 어떻게 구현할 것인가에 대한 물음에 User-Level, Kernel-Level 쓰레딩에 대해 알아보았습니다.

 

그리고 멀티쓰레딩을 구현하는 대표적이고 보편적인 Pthread 라이브러리에 대해 알아보고 코드를 리뷰해보았는데요. C언어가 아직 익숙하지 않기 때문에 코드 예제를 보면서 공부해보고 과제를 하면서도 공부해봐야할 것 같습니다.

 

감사합니다.