쓰레드(thread): 프로세스 내에서 실제로 작업을 수행하는 주체
#include <stdio.h>
#include <assert.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");
rc = pthread_create(&p1 , NULL , mythread , "A");
rc = pthread_create(&p2 , NULL , mythread , "B");
// 종료 할 수 있도록 대기 중인 쓰레드 병합하기
rc = pthread_join(p1 , NULL);
rc = pthread_join(p2 , NULL);
printf("main: end\n");
}
전역 공유 변수를 갱신하는 두 쓰레드에 대한 예제를 살펴보자.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int max = 1000000;
static volatile int counter = 0; // shared global variable
/*
* 반복문을 사용하여 단순히 1씩 더하는 함수
* 멀티쓰레드의 공유 데이터 문제를 확인할 수 있다.
*/
void *mythread(void *arg) {
char *letter = arg;
int i; // stack (private per thread)
printf("%s: begin [addr of i: %p]\n", letter, &i);
for (i = 0; i < max; i++) {
counter = counter + 1; // shared: only one
}
printf("%s: done\n", letter);
return NULL;
}
int main(int argc, char *argv[]) {
pthread_t p1, p2;
printf("main: begin [counter = %d] [%x]\n", counter, (unsigned int) &counter);
pthread_create(&p1, NULL, mythread, "A");
pthread_create(&p2, NULL, mythread, "B");
// join waits for the threads to finish
pthread_join(p1, NULL);
pthread_join(p2, NULL);
printf("main: done\n [counter: %d]\n [should: %d]\n", counter, max*2);
return 0;
}
왜 위와 같은 현상이 일어났는지 이해하기 위해 컴파일러가 생성한 코드의 실행 순서를 알아보자.
// counter 변수의 주소 = 0x8049a1c
mov 0x8049a1c, %eax // counter 읽고 eax 레지스터로 옮김
add $0x1, %eax // eax 레지스터에 1 더함
mov %eax, 0x8049a1c // eax 레지스터의 값을 counter에 넣음
두 쓰레드에서 counter의 값을 늘리는 작업을 수행하였지만, counter의 최종 값은 51이다. 즉 "정확하게" 동작되어야 한다면 counter의 값이 52가 되어야 한다.
우리는 다음 세 개의 명령어가 원자적으로 실행되기를 원한다.
// counter 변수의 주소 = 0x8049a1c
mov 0x8049a1c, %eax // counter 읽고 eax 레지스터로 옮김
add $0x1, %eax // eax 레지스터에 1 더함
mov %eax, 0x8049a1c // eax 레지스터의 값을 counter에 넣음
임계 영역 문제에 대한 해결 방법 중 하나는 "강력한 명령어" 한 개로 의도한 동작을 수행하고, 인터럽트 발생 가능성을 원천적으로 차단하는 것이다.
memory−add 0x8049a1c, $0x1
와 같은 강력한 명령어가 있다고 하자.하지만 일반적인 상황에서는 저런 명령어는 없다고 봐야한다...
따라서 우리는 동기화 함수(synchronization primitives)가 필요하고, 이는 하드웨어적으로 어셈블리 명령어 몇개로 구현할 수 있다.
결과적으로 하드웨어 동기화 명령어와 운영체제의 지원을 통해 "제대로 잘 작동하는" 멀티 쓰레드 프로그램을 작성할 수 있다.
핵심 질문: 동기화를 지원하는 방법
- 유용한 동기화 함수를 만들기 위해 어떤 하드웨어 지원이 필요?
- 운영체제는 어떤 지원?
- 어떻게 하면 이런 함수를 정확하고 효율적으로?
실제론 하나의 쓰레드가 다른 쓰레드의 동작이 완료될 때까지 대기해야 하는 상황이 빈번하게 발생한다.
이후 멀티 쓰레드 프로그램에서 배울 내용
운영체제는 최초의 병행 프로그램이었고, 운영체제 내에서 사용을 목적으로 다양한 기법들이 개발되었다.
(나중에는 멀티 쓰레드 프로그램이 등장하면서 응용 프로그래머들도 이러한 문제를 고민하게 되었다.)
즉, 왜 운영체제에서 이러한 것들은 다루는지는 한 단어로 "역사"이기 때문이다.