아래의 동시성 문제 예제 코드를 통해 동시성 문제가 무엇인지, critical section이 무엇인지 알아보자.
#include <stdio.h>
#include <pthread.h>
int sum;
void *run1(void* param){
int i;
for(int i=0; i<10000; i++)
sum++;
pthread_exit(0);//thread exit
}
void *run2(void* param){
int i;
for(int i=0; i<10000; i++)
sum--;
pthread_exit(0);
}
int main(){
phtread_t tid1, tid2;//thread tid1,tid2 생성
pthread_create(&tid, NULL, run1, NULL);
pthread_create(&tid, NULL, run2, NULL);
pthread_join(tid, NULL);
pthread_join(tid, NULL);
printf("%d \n", sum);
}
위의 예제 코드를 해석보자.
각자 10000번 더하고 10000번 빼니까 독립적으로 시행된다고 가정해보면 0이 나와야 한다. 하지만 실제로 프로그램을 실행해보면 예상 외의 결과가 나온다.
항상 같은 값을 출력하는 게 아니라, 랜덤한 값들이 나오게 된다. 왜 그럴까? 위 코드의 기계어 슈도코드를 보면 알 수 있다.
//tid1 기계어 슈도 코드
...
register1 = sum; // 데이터 영역 sum 변수의 값을 tid1의 register1으로 복사
register1 = register1+1; //register1에서 연산 시행
sum = register1; //register1에서 연산한 값을 데이터 영역 sum 변수 값에 복사
...
//tid2 기계어 슈도 코드
...
register2 = sum;
register2 = register+1;
sum = register2;
...
위의 코드를 해석해보자.
여기서 문제가 발생하게 된다. 예를 들어, sum의 값이 1이라고 가정해보자.
맨 위 예제 c코드를 보면, sum이 1인 초기조건에서 sum을 ++연산 해주는 tid1과 --해주는 tid2가 각각 실행 되었다. 따라서 sum의 값이 항상 1이 돼야 할 것 같은데, 위의 기계어 슈도코드의 실행을 따라가보니 마지막으로 sum에 복사된 값은 2가 되는 경우의 수가 나온다.
즉, 각 스레드가 어떻게 실행 되는지 실행 순서에 따라 결과값인 sum이 바뀌게 된다. 이렇게 여러 프로세스/스레드의 접근 순서나 타이밍에 의해 결과값에 무작위성이 부여 되는 것을 동시성 문제라고 한다.
단순히 c언어 예제 코드만 보면 run1, run2가 독립적으로 실행되니까 값도 각 스레드의 독립적인 실행 결과가 반영 되어야 할 것 같아 보인다.
하지만 sum이라는 전역 변수가 존재 해서, 이 두 스레드가 언제 접근해서 언제 연산결과를 반영 하느냐에 따라, 즉 프로그램 런타임 시 기계어의 실행 순서가 어떻게 동작 하느냐에 따라 서로 다른 실행결과를 만들어 내게 된다.
코드를 직관적, 정적으로 바라보지 않고, 코드가 실행되는 런타임 환경, 동적으로 컴퓨터가 어떻게 동작하는지 알면 왜 이런 동시성 문제가 생기는지 알 수 있다.
공동 영역 변수, 메모리, 파일 등에 여러 프로세스/스레드가 동시에 접근하여, 그 순서에 따라 무작위한 실행결과가 나오는 상황을 race condition이라고 한다.
위의 동시성 문제는 특정 코드 영역에서 공유자원에 동시적으로 접근하기 때문에 발생한다. 일반적으로 아래와 같은 구조를 가지게 된다.
while(true){
//entry section
//#### CRITICAL SECTION CODE ####
//exit section
//rest of the code
}
여러 프로세스/스레드에서 공유 영역에 동시에 접근하지 않으려면 위의 critical section을 동시에 실행하지 않도록 막아 줘야한다. 즉, 각 프로세스/스레드 사이에서 일종의 프로토콜을 정의 해서, 어떤 프로세스/스레드가 critical section 영역 코드를 실행하는 동안 다른 프로세스/스레드는 해당 영역을 실행하지 못하도록 알려줘야 한다.
여러 프로세스/스레드가 critical section을 동시에 실행하지 못하도록 해야 하는 프로세스/스레드 간 프로토콜 설계 문제. 이런 문제를 critical section problem(csp)이라고 한다.
그리고 이 critical section 문제에 대한 해는 아래 세 가지 조건을 만족해야 한다.
1. Mutual Exclusion : pi가 임계영역을 실행하는 동안 다른 프로세스들은 해당 영역을 실행하면 안 된다.
2. Progress (avoid dead lock) : 어떤 프로세스도 임계영역을 실행하지 않고 있을 경우, 한 프로세스가 임계영역 실행을 원한다면 그 프로세스는 임계영역에 들어갈 수 있어야 한다
3. Bound waiting (avoid starvation) : 임계영역 진입 횟수에 bound를 두어야 한다. 특정 프로세스만 계속 임계영역에 진입하면, 다른 프로세스들은 계속 임계영역 진입을 기다려야 한다(starvation).
그렇다면 이렇게 동적으로 무작위성이 부여된 코드를 정적이고 예측 가능한 코드로 바꿀 수 있을까? 바로 프로세스/스레드 간 동기화(Synchronization)을 통해 해결할 수 있다.