////
Search

3. 동기화와 교착 상태

개요

공유 자원(shared resource) : 프로세스 혹은 스레드가 공유하는 자원
EX : 메모리나 파일, 전역 변수나 입출력장치
임계 구역(critical section) : 공유 자원에 접근하는 코드 중 동시 실행했을 때 문제가 발생 가능한 코드
상황 1 : 프로세스 A가 공유 메모리 공간에 데이터를 쓰고, 프로세스 B가 해당 메모리 공간을 읽는 상황 상황 2 : 같은 프로세스의 자원을 공유하는 스레드 간 통신이 이뤄지는 상황
동시다발적으로 실행되는 프로세스 혹은 스레드를 다룰 때는, 언제나 임계 구역을 동시에 실행하지 않도록 유의해야함. 프로세스 혹은 스레드가 동시에 임계 구역 코드를 실행하여 문제가 발생하는 상황레이스 컨디션(race condition)이라함. 레이스 컨디션 발생 시 자원 일관성이 손상될 수 있기에 2개 이상의 프로세스 혹은 스레드가 임계 영역에 진입하고자 한다면 둘 중 하나는 작업이 끝날 때까지 대기해야함(레이스 컨디션은 소스 코드 상에서 발생 가능한 문제 상황)
#include <stdio.h> #include <pthread.h> int shared_data = 0 // 공유 데이터 void* increment(void* arg) { int i; for(i=0; i<100000; i++) { shared_data += 1; } return NULL; } void* decrement(void* arg) { int i; for(i=0; i<100000; i++) { shared_data -= 1; } return NULL; } int main() { pthread_t thread1, thread2; pthread_create(&thread1, NULL, increment, NULL); pthread_create(&thread2, NULL, decrement, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); printf("공유 변수 마지막 결과 : %d\n", shared_data); return 0; }
C
복사
실행마다 결과가 다르게 나옴을 알 수 있음 : 레이스 컨디션이 발생했다는 의미
레이스 컨디션을 방지하면서 임계 구역을 관리하려면, 프로세스와 스레드가 동기화(synchronization)되어야함. 동기화는 다음 2가지 조건을 준수하면서 실행하는 것을 의미 :
1.
실행 순서 제어 : 프로세스 및 스레드를 올바른 순서로 실행하기
2.
상호 배제 : 동시에 접근하면 안 되는 자원(공유 자원)에 하나의 프로세스 및 스레드만 접근하기
즉, 동기화는 “실행 순어 제어를 위한 동기화”가 있고 “상호 배제를 위한 동기화”가 존재

실행 순서 제어 동기화(예시)

#include <stdio.h> #include <pthread.h> pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int shared_resource = 0; int resource_available = 0; int turn = 0; // 소비자 차례 제어 변수 void* producer(void* arg) { pthread_mutex_lock(&lock); shared_resource = 42; // 공유 자원 생성 resource_available = 1; turn = 0; // 첫 소비자 차례 pthread_cond_broadcast(&cond); // 소비자에게 알림 pthread_mutex_unlock(&lock); return NULL; } void* consumer(void* arg) { int id = *(int *)arg; pthread_mutex_lock(&lock); while (!resource_available || turn != id) { pthread_cond_wait(&cond, &lock); // 자원이 준비될 때까지 대기 } printf("Consumed: %d by consumer %d\n", shared_resource, id); // 공유 자원 소비 turn++; // 다음 소비자에게 차례 넘기기 pthread_cond_broadcast(&cond); // 모든 대기 중인 소비자에게 알림 pthread_mutex_unlock(&lock); return NULL; } int main() { pthread_t producer_thread; pthread_t consumers[3]; int consumer_ids[3] = {0, 1, 2}; // 스레드 생성 for (int i = 0; i < 3; i++) { pthread_create(&consumers[i], NULL, consumer, (void *)&consumer_ids[i]); } pthread_create(&producer_thread, NULL, producer, NULL); // 스레드 종료 대기 pthread_join(producer_thread, NULL); for (int i = 0; i < 3; i++) { pthread_join(consumers[i], NULL); } return 0; }
C
복사
여러 스레드를 생성해 공유 자원에 접근하지만, 순서 제어를 통해 순서대로 접근하도록 만듦

상호 배제 동기화(예시)

#include <stdio.h> #include <pthread.h> pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; int shared_counter = 0; void* increment(void* arg) { for(int i=0; i<100000; i++) { pthread_mutex_lock(&lock); // 임계 구역 시작 shared_counter++; pthread_mutex_unlock(&lock); // 임계 구역 종료 } return NULL; } int main() { pthread_t t1, t2; pthread_create(&t1, NULL, increment, NULL); pthread_create(&t2, NULL, increment, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("Final counter value: %d\n", shared_counter); return 0; }
C
복사
원래라면 공유 자원에 동시 접근하므로 레이스 컨디션이 발생해 값이 일정하게 20000이 안 나왔지만, 상호 배제 동기화를 통해 20000이 출력되는 것을 보장하게 됨

동기화 기법

뮤텍스 락(mutex lock)

상호배제
동시 접근하면 안 되는 자원(공유 자원)에 동시 접근이 불가능하도록 상호 배제를 보장하는 동기화 도구
뮤텍스 락의 원리
임계 구역에 접근하고자 한다면 반드시 락(lock)을 획득(acquire)해야하고, 임계 구역에서의 작업이 끝났다면 락을 해제(release)해야함
전형적인 뮤텍스 락은, 프로세스 및 스레드가 공유하는 변수(lock)와 2개의 함수(acquire, release)로 구현
lock.acquire() // 임계 구역 lock.release()
C
복사
언어마다 문법은 차이가 있으므로, 핵심은 임계 구역 진입 전후로 뮤텍스 락을 획득하고 해제한다는 것

세마포(semaphore)

상호배제
뮤텍스 락과 비슷하지만, 조금 더 일반화된 방식의 동기화 도구 : 공유 자원이 여러 개 있는 상황에서도 동기화 가능(뮤텍스 락은 하나의 공유 자원만 고려 가능한 동기화 도구)
하나의 변수와 2개의 함수로 구성 :
변수 S : 사용 가능한 공유 자원의 개수를 나타내는 변수
wait() 함수 : 임계 구역 진입 전 호출하는 함수
1.
변수 S 값을 1 감소시켰을 때 S값이 양수면, 사용 가능한 공유 자원 개수가 존재한다는 의미 → 실행 상태로 전환(S값이 0이면 wait()를 호출한 프로세스 및 스레드는 대기 상태로 전환되어 임계 구역 진입X)
signal() 함수 : 임계 구역 진입 후 호출하는 함수
1.
먼저 “사용 가능한 공유 자원의 개수”를 나타내는 변수 S를 1 증가
2.
증가시킨 S값이 1 이상이면, 대기 상태로 접어든 프로세스 중 하나를 준비 상태로 전환
#include <stdio.h> #include <pthread.h> #include <semaphore.h> #include <unistd.h> #define NTHREADS 10 // 스레드 개수 sem_t semaphore; // 세마포어 void* worker(void* arg) { int id = (*(int *)arg); sem_wait(&semaphore); // 세마포어 획득 printf("Thread %d : 자원 사용 중\n", id); sleep(1); // 자원 사용 시뮬레이션 : 실제로는 자원을 사용하는 코드 존재 printf("Thread %d : 자원 사용 완료\n", id); sem_post(&semaphore); // 세마포어 반납 return NULL; } int main() { pthread_t threads[NTHREADS]; int thread_ids[NTHREADS]; sem_init(&semaphore, 3, MAX_RESOURCES); // 세마포어 초기화 for(int i=0; i<NTHREADS; i++) { thread_ids[i] = i; pthread_create(&threads[i], NULL, worker, (void*)&thread_ids[i]); } for(int i=0; i<NTHREADS; i++) { pthread_join(threads[i], NULL); } sem_destroy(&semaphore); // 세마포어 제거 return 0; }
C
복사
한 번에 3개의 스레드만 사용하는 형태

조건 변수 & 모니터

모니터 동기화 기법을 알려면, 우선 조건 변수 이해 필요

조건 변수

조건 변수(condition variable) : 실행 순서 제어를 위한 동기화 도구
특정 조건 하에 프로세스를 실행/중단 → 프로세스나 스레드의 실행 순서를 제어 가능. 조건 변수에 대해 wait(), signal() 함수를 호출 가능
wait() 함수 : 호출한 프로세스 및 스레드의 상태를 대기 상태로 전환하는 함수
signal() 함수 : wait()로 일시 중지된 프로세스 및 스레드의 실행을 재개하는 함수
조건 변수는 뮤텍스와 함께 사용 : 특정 조건을 만족할 때 스레드를 깨우고 뮤텍스를 획득하는 형태
pthread_cond_wait(&cond, &mutex) : 조건 만족할 때까지 대기, 깨어날 때 뮤텍스 다시 획득
pthread_cond_signal(&cond) : 하나의 대기 스레드 깨움
pthread_cond_broadcast(&cond) : 모든 대기 스레드 깨움
#include <stdio.h> #include <pthread.h> pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int shared_resource = 0; int resource_available = 0; int turn = 0; // 소비자 차례 제어 변수 void* producer(void* arg) { pthread_mutex_lock(&lock); shared_resource = 42; // 공유 자원 생성 resource_available = 1; turn = 0; // 첫 소비자 차례 pthread_cond_broadcast(&cond); // 소비자에게 알림 pthread_mutex_unlock(&lock); return NULL; } void* consumer(void* arg) { int id = *(int *)arg; pthread_mutex_lock(&lock); while (!resource_available || turn != id) { pthread_cond_wait(&cond, &lock); // 자원이 준비될 때까지 대기 } printf("Consumed: %d by consumer %d\n", shared_resource, id); // 공유 자원 소비 turn++; // 다음 소비자에게 차례 넘기기 pthread_cond_broadcast(&cond); // 모든 대기 중인 소비자에게 알림 pthread_mutex_unlock(&lock); return NULL; } int main() { pthread_t producer_thread; pthread_t consumers[3]; int consumer_ids[3] = {0, 1, 2}; // 스레드 생성 for (int i = 0; i < 3; i++) { pthread_create(&consumers[i], NULL, consumer, (void *)&consumer_ids[i]); } pthread_create(&producer_thread, NULL, producer, NULL); // 스레드 종료 대기 pthread_join(producer_thread, NULL); for (int i = 0; i < 3; i++) { pthread_join(consumers[i], NULL); } return 0; }
C
복사

모니터

공유 자원과 해당 공유 자원을 다루는 함수(인터페이스)로 구성된 동기화 도구. 상호 배제를 위한 동기화뿐만 아니라 실행 순서 제어를 위한 동기화까지 가능
상호 배제를 위한 동기화 : 프로세스 및 스레드는 공유 자원에 접근하기 위해 반드시 정해진 공유 자원 연산(인터페이스)를 통해 모니터 내로 진입. 이때 모니터 안에 진입해 실행되는 프로세스 및 스레드는 항상 하나(이미 모니터 내로 진입해 실행 중인 프로세스 및 스레드가 있다면 큐에서 대기)
실행 순서 제어를 위한 동기화 : 조건 변수를 함께 활용하면 구현 가능
EX) 자바의 synchronized 등등..

스레드 안전

멀티스레드 환경에서 어떤 변수나 함수, 객체에 동시 접근이 이뤄져도 실행에 문제가 없는 상태(= 성질 O, 도구 X)

교착 상태(Deadlock)

프로세스 실행을 위해서는 자원이 필요 → 2개 이상의 프로세스가 각자 가지고 있는 자원을 무작정 기다린다면, 더 이상 어떤 프로세스도 실행할 수 없는 교착 상태 발생 가능
교착 상태(deadlock) : 일어나지 않을 사건을 기다리며 프로세스 진행이 멈춰버리는 현상

교착 상태 발생 조건

교착 상태가 발생 상황에는 4가지 필요 조건 존재. 이 4가지 조건이 모두 만족할 때, 교착 상태가 발생할 가능성이 생김

1) 상호 배제

교착 상태 발생의 근본 원인 : 한 번에 하나의 프로세스만 해당 자원을 이용 가능했기 때문
즉, 한 프로세스가 사용하는 자원을 다른 프로세스가 사용불가한 상호 배제의 상황에서 교착 상태 발생 가능

2) 점유 & 대기

한 프로세스가 어떤 자원을 할당받은 상태(점유)에서 다른 자원을 할당받기를 기다리는 경우(대기), 교착 상태 발생 가능

3) 비선점

자원이 비선점 : 해당 자원을 이용하는 프로세스의 작업이 끝나야만 비로소 자원 이용 가능 (즉, 비선점 또한 교착 상태의 근본적 문제)
어떤 프로세스도 다른 프로세스의 자원을 강제로 못 뺏는 경우 교착 상태 발생 가능

4) 원형 대기

프로세스와 프로세스가 요청한 자원이 원의 형태를 이루는 경우. 각 프로세스가 서로 점유한 자원을 할당받기 위해 원 형태로 대기할 경우 교착 상태 발생 가능

교착 상태의 해결 방법

1) 교착 상태 예방

사전 조치
교착 상태를 발생시키는 4가지 필요 조건(상호배제, 점유&대기, 비선점, 원형대기) 중 하나를 충족하지 못 하게 하는 방법 :
1.
상호배제 깨기
공유 불가능한 자원 제거, 자원을 가상화해서 여러 프로세스가 동시 접근 가능하도록 하기
2.
점유와 대기 깨기
프로세스가 실행 시작 전에 필요한 모든 자원을 한 번에 요청
3.
비선점 깨기
어떤 프로세스가 필요한 자원을 얻지 못하면, 이미 보유한 자원을 반납
4.
순환 대기 깨기
자원에 고유 번호 부여 → 프로세스가 자원 요청 시 반드시 번호가 증가하는 순서대로 요청하게 강제

2) 교착 상태 회피

사전 조치
교착 상태가 발생하지 않을 정도로만 조심하면서 자원을 할당하는 방법 : 회피 방법은 기본적으로 교착 상태를 한정된 자원의 무분별한 할당으로 인해 발생하는 문제로 간주
자원이 한정된 상황에서 모든 프로세스들이 한 번에 많은 자원을 요구한다면, 교착 상태 발생 위험 증가

3) 교착 상태 검출 후 회복

사후 조치
운영체제가 프로세스의 자원 요구 상황마다 그때 그때 자원을 할당하고 주기적으로 교착 상태 여부를 검사. 그러다 교착 상태가 검출되면 프로세스를 자원 선점을 통해 회복시키거나, 교착 상태에 놓인 프로세스를 강제 종료함으로써 회복 가능.
프로세스를 자원 선점을 통해 회복 = 교착 상태가 해결될 때까지 다른 프로세스로부터 강제로 자원을 빼앗아 한 프로세스에 몰아서 할당하는 것