<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