1. 병행성: 개요

  • 멀티 쓰레드 프로그램은 하나 이상의 실행 지점(독립적으로 불러 들여지고 실행될 수 있는 여러 개의 PC 값)을 가지고 있다.
  • 멀티 쓰레드를 이해하는 다른 방법은 각 쓰레드가 프로세스와 매우 유사하지만, 차이가 있다면 쓰레드들은 주소 공간을 공유하기 때문에 동일한 값에 접근할 수 있다는 것이다.
  • 쓰레드는 어디서 명령어들을 불러 들일지 추적하는 프로그램 카운터(PC)와 연산을 위한 레지스터들을 가지고 있다.
  • 두 개 이상의 쓰레드가 하나의 프로세서에서 실행 중이라면 문맥 교환이 이루어져야 한다.
    • 문맥 전환을 할 때 프로세스는 자신의 상태를 프로세스 제어 블럭(process control block, PCB)에 저장하듯이 쓰레드는 쓰레드 제어 블럭(thread control block, TCB)에 저장한다.
    • 가장 큰 차이 중 하나는 프로세스의 경우와 달리 쓰레드 간의 문맥 교환에서는 주소 공간을 그대로 사용한다.
    • 쓰레드와 프로세스의 또 다른 차이는 쓰레드는 자신만의 스택이 존재한다.

1.1 왜 쓰레드를 사용하는가?

  • 쓰레드를 사용해야하는 2가지 주요 이유가 있다.
    1. 병렬 처리(parallelism)
    2. 느린 I/O로 인해 프로그램 실행이 멈추지 않도록하기 위해 쓰레드를 사용한다.

쓰레드는 병렬처리가 가능한데 문맥전환을 한다…?! 이 말의 뜻은 만약 프로세서가 1개일 때는 쓰레드가 문맥전환을 이룬다 <- 이것도 멀티 프로세스보다 성능이 좋은데 그 이유는 문맥전환의 비용이 쓰레드가 더 적기 때문이다.

2번의 추가 예로는 예를들어 한 프로세스에서 파일 입출력 시그널이 발생했다. 원래 같으면 해당 프로세스는 블록되고 다른 프로세스로 제어가 넘어가야한다. 하지만 멀티 쓰레드라면 파일 입출력이 진행 되는 동안 같은 프로세스에서 다른 쓰레드가 다른 작업을 이어 나갈 수 있다.

1.2 예제: 쓰레드 생성

    #include <stdio.h>
    #include <pthread.h>
    
    void *mythread(void *arg) {
        printf("%s\n", (char *) arg);
        return NULL;
    }
    
    int main (int argc, char *argv[]) {
        pthread_t p1, p2;
        int rc;
        printf("main: begin\n");
        pthread_create(&p1, NULL, mythread, "A");
        pthread_create(&p2, NULL, mythread, "B");
        // 종료할 수 있도록 대기 중인 쓰레드 병합하기
        pthread_join(p1,NULL);
        pthread_join(p2,NULL);
        printf("main: end\n");
        return 0;
    }
  • 위의 코드를 실행하면, A B 또는 B A가 출력된다.
    • 생성된 쓰레드는 호출자와는 별개로 실행된다는 것을 확인할 수 있다.
    • 다음에 실행될 쓰레드는 OS 스케줄러(scheduler)에 의해 결정된다.

1.3 훨씬 더 어려운 이유: 데이터의 공유

    #include <stdio.h>
    #include <pthread.h>
    
    static volatile int counter = 0;
    
    // mythread()
    // 반복문을 사용하여 단순히 1씩 더하기
    // 10,000,000을 변수 counter에 더하는 방법이 아니다.
    // 하지만, 문제가 무엇인지 명확하게 해준다.
    void *mythread(void *arg) {
        printf("%s: begin\n", (char *) arg);
        int i;
        for (i=0; i < 1e7; i++){
            counter = counter + 1;
        }
        printf("%s: done\n", (char *) arg);
        return NULL;
    }
    
    // main()
    // 두 개의 쓰레드를 실행하고 (pthread_create)
    // 대기한다 (pthread_join)
    int main (int argc, char *argv[]) {
        pthread_t p1, p2;
        printf("main: begin (counter = %d)\n", counter);
        pthread_create(&p1, NULL, mythread, "A");
        pthread_create(&p2, NULL, mythread, "B");
        // 종료할 수 있도록 대기 중인 쓰레드 병합하기
        pthread_join(p1,NULL);
        pthread_join(p2,NULL);
        printf("main: done with both (counter = %d)\n", counter);
        return 0;
    }
  • 위의 코드를 실행하면 출력으로 20000000이 나올 줄 알았지만, 결과는 매번 다르다. ex) 187777789, 15796892
    • 왜 이런 결과가 나오는지 궁금하다면 더 읽어보자!

1.4 문제의 핵심: 제어 없는 스케줄링

    mov 0x8049alc, %eax
    add $0x1, %eax
    mov %eax, 0x8049a1c
  • mythread 함수의 for문을 역 어셈블리 하면 위와 같은 결과를 가진다.
    • 위의 영역을 두 쓰레드가 동시에 접근하기 때문에 결과가 달라지는 것이다.


  • 명령어의 실행 순서에 따라 결과가 달라지는 상황을 경쟁 조건(race condition) 혹은 데이터 경쟁(data race)라고 한다.
  • 멀티 쓰레드가 같은 코드를 실행할 때 경쟁 조건이 발생하기 때문에 이러한 코드 부분을 임계 영역(critical section)이라고 부른다.
    • 공유 변수(공유 자원)을 접근하고 하나 이상의 쓰레드에서 동시에 실행되면 안 되는 코드를 임계 영역이라 부른다.
  • 이러한 코드에서 필요한 것은 상호 배제(mutual exclusion)이다.
    • 이 속성은 하나의 쓰레드가 임계 영역 내의 코드를 실행 중일 때는 다른 쓰레드가 실행할 수 없도록 보장해준다.

1.5 원자성에 대한 바람

  • 임계 영역 문제에 대한 해결 방법 중 하나로 강력한 명령어 한 개로 의도한 동작을 수행하여, 인터럽트 발생 가능성을 원천적으로 차단하는 것이다.
    • 락과 같은 기능을 사용한다.
  • 우리가 대신 해야 할 일은 하드웨어 동기화 함수(synchronization primitives) 구현에 필요한 몇 가지 유용한 명령어를 요청하는 것이다.

태그: ,

카테고리:

업데이트:

댓글남기기