ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [OS101] 쓰레드
    테크 2023. 9. 14. 17:38

     

    본 글은 2023.09.14에 새롭게 업데이트되었습니다.

     

    서울대학교 평생 교육원에서 제공하는 운영체제의 기초 강의 내용을 중점으로 정리했다. 부족한 부분은 <Operating System Concepts> 책을 참고하면서 채워 나갔다. 이해를 돕기 위한 그림들은 여기에서 가져왔다.

     

    목차

    • 쓰레드란 무엇인가
    • 쓰레드가 필요한 이유
    • 쓰레드 구현 형태
    • 멀티쓰레딩 모델
    • 쓰레드 라이브러리 - pthreads

     

    쓰레드란 무엇인가

    쓰레드(thread)는 CPU의 실행 단위이다. 쓰레드의 개념을 정확하게 알기 위해서는 반드시 프로세스 개념을 정확하게 알아야 한다. 프로세스에는 컨텍스트와와 실행 흐름(execution stream 또는 thread of control)이 있다. 이제 이 두 개를 분리시키자. thread of control, 즉 실행 흐름을 분리시킨 것이 쓰레드이다. 쓰레드는 프로세스의 코드 영역, 데이터 영역, 힙 영역을 공유한다. 실행 흐름을 분리시켰기 때문에 스택은 공유하지 않는다. 이 때문에 경량 프로세스(light weight process)라고도 부른다. 한 프로세스에 여러 개의 쓰레드가 존재할 수 있으므로 멀티쓰레딩(multi-threading)이라는 단어가 생겨났다. 멀티 쓰레딩을 하게 되면 쓰레드들이 공유하는 영역이 존재하기 때문에 프로세스간 통신(Inter-Process Communications, IPC)도 간단하며, 쓰레드 간의 컨텍스트 스위칭(contrext switching)을 하는 것도 쉽다.

     

    싱글 쓰레드와 멀티쓰레드

     

    쓰레드가 필요한 이유

    • 적은 비용으로 concurrency를 얻기 위해서이다. 즉 응답 속도를 줄이고자 개념이 생겨났다. 예를 들어 웹브라우저를 통해서 매우 큰 파일을 다운로드 받고자 할 때, 쓰레드가 하나만 존재한다면 다운로드를 받는 동안 아무것도 할 수 없을 것이다.
    • Massively parallel scientific programming을 할 때 발생하는 오버헤드를 줄이기 위해서이다. 오버헤드가 줄어드는 이유는 프로세스를 여러 개 생성하지 않기 때문에 시스템 과부하를 줄일 수 있기 때문이다. 쓰레드는 거의 스택만 생성하기 때문에 메모리 요구량이 적다. 프로세스를 새로 생성하는 것이 아니기 때문에 fork() 할 필요가 없다.
     
    쓰레드들이 프로세스에 할당된 리소스들을 공유할 수 있는 장점이 있지만 동기화 문제가 발생하기 때문에 각별한 주의가 필요하다.
     

    쓰레드 구현 형태

    • 유저 레벨 쓰레드 (User-Level Thread): 커널이 전혀 모르게 유저 레벨에서 쓰레드를 구현하는 방법이다. 이 때 TCB(Thread Control Block)들은 모두 유저 레벨에 둔다. 
    • 커널 레벨 쓰레드 (Kernel-Level Thread): 커널 레벨에서 동작하는 쓰레드이다. 

     

    멀티쓰레딩 모델

    • Many-To-One Model
    • One-To-One Model
    • Many-To-Many Model

     

    Many-To-One Model

    여러 개의 유저 레벨 쓰레드가 하나의 커널 레벨 쓰레드와 매핑된 모델이다. 이 모델에서 커널 레벨 쓰레드는 유저 레벨 쓰레드가 여러 개가 있는지 알지 못한다. 쓰레드 관리 자체가 유저 레벨에서 진행하기 때문에 굉장히 효율적이다. 쓰레드의 스케줄링은 유저 레벨에서만 일어난다. 이 때 유저 레벨에 있는 스케줄링 라이브러리를 유저 레벨 쓰레드 라이브러리(User-Level Threads Library)라고 한다. 이 라이브러리 안에는 쓰레드 생성/소멸 코드, 쓰레드의 컨텍스트 저장 및 쓰레드 간의 커뮤니케이션 함수가 존재한다.

    그러나 유저 레벨에서 쓰레드 스케줄링 라이브러리를 만드는 것은 매우 어렵다. 스케줄링에는 선점적 스케쥴링(preemptive scheduling)과 비선점적 스케쥴링(non-preemptive scheduling)이 존재한다. 비선점적 스케줄링은 CPU yield만 호출하면 되므로 구현이 어렵지 않다. 하지만 선점적 스케줄링의 경우 한계점이 발생한다. 이 때는 반드시 인터럽트가 필요하다. 그러나 인터럽트를 처리하려고 할 때 커널은 프로세스 내부에 존재하는 쓰레드의 존재를 모르기 때문에 전달 방법이 없다. 한 쓰레드가 시스템 호출을 한다고 생각할 때 해당 프로세스 전체가 블락 당한다. 그러면 프로세스에 존재하는 또 다른 쓰레드들도 전부 블락당하게 된다.

    물론 불편한 점만 있는 방법인 것 같지만 장점도 존재한다. 운영체제를 고치지 않고도 반쪽 자리 멀티쓰레딩이 가능하다. 또한 병렬 연산만 한다면 복잡한 스케줄링이 필요없으므로 좋다.

     

    many-to-one model

     

    One-To-One Model

    커널 레벨 쓰레드가 하나씩 유저 레벨 쓰레드를 담당하고 있다. Many-To-One Model의 경우 한 쓰레드가 시스템 콜을 호출하면 다른 프로세스 내에 있는 쓰레드가 전부 블락당하는 문제점이 있었지만, 이 경우에는 시스템 콜을 하지 않은 쓰레드가 이미 다른 커널 레벨 쓰레드와 일대일 매핑이 되어 있으므로 문제가 없다. 하지만 커널 단의 수행 시간이 증가하면서 추가적인 오버헤드가 생긴다. 만약 유저 레벨 쓰레드의 개수가 급격히 증가한다면 커널 단 수행 시간도 증가할 것이다.

     

    one-to-one model

     

    Many-To-Many Model

    Many-To-One Model과 One-To-One Model의 장점을 섞은 모델이다. 커널 레벨 쓰레드는 유저 레벨 쓰레드의 개수와 같거나 작다. One-To-One Model과는 달리 유저 레벨 쓰레드를 많이 생성해도 큰 문제가 발생하지 않는다. Many-To-One Model과는 달리 한 쓰레드가 시스템 콜을 호출한다고 하더라도 같은 프로세스 내에 있는 다른 쓰레드가 차단당할 일은 없다. 그리고 커널 레벨 쓰레드에서 유저 레벨 쓰레드의 존재를 알고 있기 때문에 인터럽트를 건다고 해서 문제되지 않는다. 따라서 선점적 쓰레드 스케줄링(우선순위 스케줄링)을 자유롭게 할 수 있다.

     

    many-to-many model

     

    쓰레드 라이브러리 - pthread

    pthread는 POSIX thread의 약자이다. UNIX 계열 POSIX 시스템에서 병렬 프로그래밍을 도와주는 라이브러리이다. pthread를 이용해서 어떻게 쓰레드를 구현하는지 살펴보겠다.

    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int sum; /* this data is shared by the thread(s) */
    void *runner(void *param); /* the thread */
    
    int main(int argc, char *argv[]) {
        pthread_t tid; /* the thread identifier */
        pthread_attr_t attr; /* set of thread attributes */
    
        if(argc != 2) {
            fprintf(stderr, "usage: a.out <integer value>\n");
            return -1;
        }
        if (atoi(argv[1]) < 0) {
            fprintf(stderr, "%d must be >= 0\n", atoi(argv[1]));
            return -1;
        }
    
        /* get the default attributes */
        pthread_attr_init(&attr);
        /* create the thread */
        pthread_create(&tid, &attr, runner, argv[1]);
        /* wait for the thread to exit */
        pthread_join(tid, NULL);
    
        printf("sum = %d\n", sum);
    }
    
    /* The thread will begin control in this function */
    void *runner(void *param) {
        int i, upper = atoi(param);
        sum = 0;
    
        for(i = 1; i <= upper; i++) {
            sum += i;
        }
        pthread_exit(0);
    }
    • pthread_attr_init 함수:  쓰레드의 특성을 초기화하는 함수이다. 이 때 스택의 깊이, 쓰레드의 우선순위 등이 기본값으로 초기화된다. pthread_attr_init 함수의 리턴값은 0이며, 초기화된 특성은 attr 변수에 전달된다.
    • pthread_create 함수: 새로운 쓰레드를 생성하는 함수이다. 이 코드에서는 pthread_create 함수를 하나만 호출했으니 실제 동작하는 쓰레드는 1) main 함수를 실행하고 있는 쓰레드와 2) 새롭게 생성한 쓰레드 두 개이다. pthread_create 함수의 리턴값은 생성된 쓰레드 아이디이다. 첫번째 인자는 리턴값과 동일한 생성된 쓰레드 아이디이다. 두번째 인자는 새롭게 만들 쓰레드의 특성이다. 세번째 인자는 생성된 쓰레드가 어떤 동작을 할지 나타낸다. 마지막 인자는 쓰레드 동작을 정의한 함수에서 필요한 파라미터를 나타낸다. 예제에서 만약 argv[1] 값에 10이 들어갔다면, runner는 1부터 10까지의 합을 계산할 것이다.
    • pthread_join 함수: 생성된 쓰레드가 종료될 때까지 기다리는 함수이다. 리턴값은 종료된 쓰레드의 아이디이다. 첫번째 인자는 우리가 기다리는 쓰레드의 아이디를 뜻한다. 두번째 인자는 해당 쓰레드의 결과값을 저장하는 인자이다. 위 코드에서 NULL을 준 이유는 결과값을 이미 전역변수로 저장하고 있기 때문이다.

     

    만약 쓰레드를 여러 개 생성하고 싶다면 pthread_create 함수를 여러번 호출해주면 된다. 각각의 쓰레드 동작을 다르게 하고 싶다면 쓰레드 동작 함수를 개별적으로 만들어서 pthread_create 함수의 세번째 인자에 넣어주면 된다.

Designed by Tistory.