<process.c>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char* argv[]){
int pid = fork();
if(pid == -1){
return -1;
}
printf("Process id %d\n", getpid());
if(pid != 0){
wait(NULL);
}
return 0;
}
<thread.c>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* routine(){
printf("Process id %d\n", getpid());
}
int main(int argc,char *argv[]){
printf("Process id %d\n", getpid());
pthread_t t1, t2;
if(pthread_create(&t1, NULL, &routine, NULL)){
return 1;
}
if(pthread_create(&t2, NULL, &routine, NULL)){
return 2;
}
if(pthread_join(t1, NULL)){
return 3;
}
if(pthread_join(t2, NULL)){
return 3;
}
return 0;
}
|
|
왼쪽 그림이 프로세스 프로그램의 결과, 오른쪽 그림이 thread 프로그램의 결과이다. 프로세스 프로그래음 fork를 통해 자식 프로세스를 만든 상황이고, 쓰레드 프로그램은 pthread_create 를 통해 thread를 생성한 상황이다.
그림의 결과 같이 fork를 통해 생성된 프로세스와 부모 프로세스의 pid가 다른 반면, thread는 동일한 프로세스 내에 생성된 쓰레드로 같은 pid 값을 가지고 있음을 알 수 있다.
<process.res.c>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int x = 2;
int main(int argc, char* argv[]) {
int pid = fork();
if (pid == -1) {
return 1;
}
if (pid == 0) {
x++;
}
sleep(1);
printf("Process->Value of x: %d\n", x);
if (pid != 0) {
wait(NULL);
}
return 0;
}
<thread.res.c>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int x = 2;
void* thread_function1(void* arg) {
x++;
printf("Thread->Value of x: %d\n", x);
sleep(1);
return NULL;
}
void* thread_function2(void* arg) {
printf("Thread->Value of x: %d\n", x);
sleep(1);
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, thread_function1, NULL);
pthread_create(&thread2, NULL, thread_function2, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
|
|
이번 예제는 thread와 process의 자원 공유 차이를 알아보기 위해 구현한 프로그램이다. 두 프로그램 다 전역 변수로 int x = 2; 를 선언했다. 전역 변수는 메모리 영역 중 data 영역에 저장되는 부분으로 thread는 스택을 제외한 메모리 영역은 같은 프로세스에서는 공유하므로 위의 오른쪽 그림과 같은 결과가 나왔다.
첫 번째 thread가 x를 1 증가시키고 출력한 값이 3, 두 번째 thread는 연산없이 바로 출력하므로 3이 출력됨을 확인할 수 있다. 만약 data 영역을 공유하지 않는다면 두 번째 thread에서 x 값으로 2가 나와야 할 것 이다!
<mutex_unlock.c>
#include <stdio.h>
#include <pthread.h>
int num = 0;
void *increase_number(void *args)
{
for (int i = 0; i < 5000000; i++)
num++;
}
int main() {
void *result;
pthread_t thread[20];
printf("Before num: %d\n", num);
for (int i = 0; i < 20; i++){
pthread_create(&thread[i], NULL, increase_number, NULL);
}
for (int i = 0; i < 20; i++) {
pthread_join(thread[i], &result);
}
printf("After num: %d\n", num);
}
해당 프로그램은 20개의 thread를 생성해 각 쓰레드는 전역 변수 num에 for을 사용해 총 5,000,000 값을 더하는 로직을 실행하는 프로그램이다.
해당 코드의 결과 값이 예상되나?
100,000,000 값을 예상했지만 아쉽게도 결과 값은 아래와 같이 프로그램 실행할 때마다 다르다!
|
|
|
왜 20 * 5,000,000 = 100,000,000 라는 결과가 안 나올까?
둘 이상의 스레드가 공유 자원을 읽거나 쓰면서 결과값이 달라질 수 있는 Race Condition 현상이 발생하기 때문이다.
num++ 연산이 원자적이지 않기 때문인데, 원자적이지 않다라는 것은 c 언어 코드상으로는 num++ 로 한 줄이지만 실제로는 아래와 같은 세 단계로 이루어진다.
num 값을 읽기이 과정 중 다른 스레드가 끼어들어 num 변수 값이 실제 값과 다르게 읽히거나 쓰이거나 할 수 있다.
그러므로 20개나 되는 스레드의 실행 순서는 예측이 불가능하며 한 스레드의 연산 중간에 다른 스레드가 끼어들어 매 프로그램 실행마다 결과 값이 달라진다.
이제 이러한 race condition을 해결하는 다양한 방법이 있는데 지금부터 그 방법 중 하나인 mutex로 해결해보겠다.
쉽게 말해 각 스레드끼리 상호배제(Mutual Exclusion)되도록 하는 기술이다.

공유 자원(shared resource)이나 임계영역(critical section)에 각 쓰레드들의 running time이 서로 겹치지 않게 하나의 process or thread 만 접근하도록 하는 기법이다.
각 쓰레드가 공유하고 있는 데이터를 둘 이상의 쓰레드가 같이 접근하는 것을 막는데 쉽게 말해 한 쓰레드가 공유 데이터를 사용 중이면 lock을 걸어 다른 쓰레드들이 wait하게 만들고 데이터를 다 사용하면 unlock으로 해제해 다른 쓰레드가 다시 접근할 수 있도록 만든다.
mutex 기법을 사용하면 이전과 달리 공유 데이터를 순차적으로 접근하므로 공유 데이터에 대한 lock이 풀릴 때까지 wait해야 하는 추가적인 시간이 소요돼 남발되는 쓰레드 수 증가와 mutex 사용은 오히려 프로그램의 성능을 저해할 수 있다.
<mutex_lock.c>
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int num = 0;
void *increase_number(void *args)
{
pthread_mutex_lock(&mutex);
for (int i = 0; i < 5000000; i++)
num++;
pthread_mutex_unlock(&mutex);
}
int main() {
void *result;
pthread_t thread[20];
printf("Before num: %d\n", num);
for (int i = 0; i < 20; i++){
pthread_create(&thread[i], NULL, increase_number, NULL);
}
for (int i = 0; i < 20; i++) {
pthread_join(thread[i], &result);
}
printf("After num: %d\n", num);
}

자, 이제 우리가 예상한 100,000,000 값이 출력된다. 코드를 살펴보면 쓰레드들의 공유 자원인 num변수가 존재하는데 어떤 쓰레드가 num에 대한 제어권을 차지하면 다른 쓰레드들이 접근하지 못 하도록 pthread_mutex_lock 메소드를 통해 num을 증가하는 작업 동안 잠근다. 그리고 for 문이 완료된 후 num에 대한 제어권을 반환하기 위해 pthread_mutex_unlock 메소드로 해제한다.
개념적으로는 직관적이고 심플하다고 생각한다. 누군가 공유 자원을 사용하고 있으면 다른 누군가가 사용하지 못 하게 막고, 내가 다 사용하면 다시 반환하고 이렇게 생각하면 좋을 듯 하다.
두 이상의 쓰레드나 프로세스 간의 공유 자원에 대한 race condition 상황을 방지하기 위해 mutext와 같은 기법을 사용하다가 다른 문제를 직면할 수 있다. 주로 starvation(기아 상태)와 deadlock(교착 상태) 등이 있는데 여기선 데드락에 대해 한번 알아보겠다.

데드락은 두개 이상의 쓰레드가 lock 기법 등을 이용해 공유 자원 or 임계 영역을 접근할 때 서로의 작업이 완료되기를 기다리는 상태를 의미한다. 데드락이 발생되면 어떠한 쓰레드도 작업을 완료하지 못 해, 서로의 작업만 끝나기를 기다리는 무한 대기 상태가 발생한다.
데드락이 발생하기 위해서 모두 충족해야하는 필요조건이 4가지가 존재한다.
위의 4가지 모두가 데드락의 필요조건이므로 위 4가자 중 하나라도 해결한다면 데드락을 피할 수 있다.
<deadlock.c>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void* thread1_function(void* arg) {
printf("Thread 1 trying to lock mutex1\n");
pthread_mutex_lock(&mutex1);
printf("Thread 1 locked mutex1\n");
sleep(1);
printf("Thread 1 trying to lock mutex2\n");
pthread_mutex_lock(&mutex2);
printf("Thread 1 locked mutex2\n");
// 임계 영역
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void* thread2_function(void* arg) {
printf("Thread 2 trying to lock mutex2\n");
pthread_mutex_lock(&mutex2);
printf("Thread 2 locked mutex2\n");
sleep(1);
printf("Thread 2 trying to lock mutex1\n");
pthread_mutex_lock(&mutex1);
printf("Thread 2 locked mutex1\n");
// 임계 영역
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, thread1_function, NULL);
pthread_create(&thread2, NULL, thread2_function, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}

해당 코드를 통해 데드락 상황을 연출할 수 있다.
각각의 mutex lock을 공유 자원이라고 볼 때, thread1 과 thread2는 각각 mutex1, mutex2의 lock을 먼저 점유한 뒤에 다음 mutex를 점유하기 위해 lock이 해제할 때 까지 기다리는 상태가 된다. 하지만 각 쓰레드들은 두번째 mutex를 가지기 위해서는 반대 쓰레드가 작업을 완료해 첫번째 mutex에 대한 lock을 해제해야 하는데 서로 똑같은 lock을 얻지 못 해 작업을 완료하지 못 하고 무한정 대기 상태가 된다. 바로 이러한 상황을 교착 상태의 한 예라고 할 수 있다.
<deadlock_solve.c>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void* thread1_function(void* arg) {
printf("Thread 1 trying to lock mutex1\n");
pthread_mutex_lock(&mutex1);
printf("Thread 1 locked mutex1\n");
sleep(1);
printf("Thread 1 trying to lock mutex2\n");
pthread_mutex_lock(&mutex2);
printf("Thread 1 locked mutex2\n");
// 임계 영역
printf("Thread 1 in critical section\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void* thread2_function(void* arg) {
printf("Thread 2 trying to lock mutex1\n");
pthread_mutex_lock(&mutex1);
printf("Thread 2 locked mutex1\n");
sleep(1);
printf("Thread 2 trying to lock mutex2\n");
pthread_mutex_lock(&mutex2);
printf("Thread 2 locked mutex2\n");
// 임계 영역
printf("Thread 2 in critical section\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, thread1_function, NULL);
pthread_create(&thread2, NULL, thread2_function, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("Both threads completed successfully\n");
return 0;
}
위의 코드는 기존의 코드를 살짝 변형해 데드락을 회피하는 상황을 구현한 코드이다. 앞서 말한 데드락의 4가지의 필수 조건중 순환 대기를 회피한 예이다.
기존의 데드락이 발생하는 코드의 로직은 다음과 같았다.
|
Thread1
1. mutex1 점유 2. sleep 3. mutex2 점유 4. mutex 해제 후 작업완료 |
Thread2
1. mutex2 점유 2. sleep 3. mutex1 점유 4. mutex 해제 후 작업완료 |
해당 시나리오에서 쓰레드 1과 2는 서로 다른 mutex를 순서대로 점유하다가 lock이 해제되지 않아 무한 대기 하는 상황이 발생한다.
하지만 데드락을 회피한 코드의 로직은 다음과 같다.
|
Thread1
1. mutex1 점유 2. sleep 3. mutex2 점유 4. mutex 해제 후 작업완료 |
Thread2
1. mutex1 점유 2. sleep 3. mutex2 점유 4. mutex 해제 후 작업완료 |
두 쓰레드가 항상 같은 순서(mutex1 → mutex2)로 mutex lock을 획득하므로 데드락이 발생하지 않는다. 한 쓰레드가 mutex1을 획득하면 다른 스레드는 mutex1이 해제될 때까지 대기하게 되어, 서로 교차하여 락을 획득하는 상황이 발생하지 않아 무한 대기 상태를 회피할 수 있다.
https://nomad-programmer.tistory.com/114
https://heeonii.tistory.com/14
https://velog.io/@dodozee/뮤텍스Mutex와-세마포어Semaphore