운영체제 락 개념과 동기화 기법 (1. 운영체제에서 동기화 / 1-2. 임계 영역(Critical Section)과 해결 방법)

2025. 2. 26. 12:24정보기술/운영체제 (OS)

📖 1-2. 임계 영역(Critical Section)과 해결 방법

멀티스레딩 환경에서 여러 개의 스레드가 동시에 실행될 때 공유 자원(Shared Resource)에 동시에 접근하면 데이터 손상(Data Corruption) 또는 예상치 못한 동작이 발생할 수 있습니다.
이러한 문제가 발생하는 코드 영역을 임계 영역(Critical Section) 이라고 하며, 이를 해결하기 위해 동기화 기법(Synchronization Techniques) 이 필요합니다.


✅ 임계 영역(Critical Section)이란?

🔹 정의

임계 영역(Critical Section)이란 여러 개의 스레드가 동시에 실행될 경우, 충돌이나 오류가 발생할 가능성이 있는 코드 영역을 의미합니다.

예제: 임계 영역이 필요한 상황

  • 데이터베이스 업데이트: 두 개의 스레드가 동시에 같은 데이터베이스 레코드를 수정하면 데이터가 손실될 수 있음.
  • 파일 쓰기(File Writing): 여러 개의 프로세스가 동시에 같은 파일을 수정하면 데이터가 꼬일 수 있음.
  • 네트워크 요청(Network Request): 동일한 리소스를 다수의 클라이언트가 접근하면 충돌이 발생할 수 있음.

✅ 임계 영역 문제 예제 (C 코드)

동기화 없이 balance 값을 수정하는 코드에서 경쟁 상태(Race Condition) 가 발생할 수 있습니다.

#include <stdio.h>
#include <pthread.h>

int balance = 1000; // 공유 자원 (임계 영역)

void* withdraw(void* arg) {
    int temp = balance; // 현재 잔액 읽기
    temp -= 500;        // 인출 (임계 영역)
    balance = temp;     // 수정된 값 저장
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, withdraw, NULL);
    pthread_create(&t2, NULL, withdraw, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("최종 잔액: %d\n", balance); // 500이 아닐 수도 있음!
    return 0;
}

🔴 실행 결과 예측 불가

최종 잔액: 500
최종 잔액: 1000
최종 잔액: 0  <-- 데이터 손상 발생 가능!

원인:

  • Thread 1과 Thread 2가 동시에 balance -= 500 연산을 수행하면서 중간 연산 결과가 덮어씌워지는 문제 발생
  • 이로 인해 잔액이 0이 될 수도 있고, 500이 유지되지 않을 수도 있음!
  • 해결 방법: 임계 영역을 보호해야 함.

✅ 임계 영역 해결 방법

임계 영역에서 발생하는 문제를 해결하는 주요 방법은 다음과 같습니다.

해결 방법 설명
1. 락(Lock) 사용 한 번에 하나의 스레드만 임계 영역에 접근하도록 제한
2. 스레드 간 순서 조정 (Condition Variable 사용) 특정 조건이 충족될 때만 스레드가 실행되도록 설정
3. 원자적 연산(Atomic Operation) 활용 CPU에서 지원하는 원자적 연산을 사용하여 충돌 방지

✅ 1. 락(Lock) 사용 - Mutex로 임계 영역 보호

락(Lock)을 사용하면 한 번에 하나의 스레드만 임계 영역에 접근할 수 있도록 제한하여 경쟁 상태를 방지할 수 있습니다.

🟢 해결 코드: Mutex 적용

#include <stdio.h>
#include <pthread.h>

int balance = 1000; // 공유 자원 (임계 영역)
pthread_mutex_t lock; // 뮤텍스 선언

void* withdraw(void* arg) {
    pthread_mutex_lock(&lock);  // 🔒 락 획득 (임계 영역 보호)
    
    int temp = balance;
    temp -= 500;
    balance = temp;

    pthread_mutex_unlock(&lock); // 🔓 락 해제
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_mutex_init(&lock, NULL); // 뮤텍스 초기화

    pthread_create(&t1, NULL, withdraw, NULL);
    pthread_create(&t2, NULL, withdraw, NULL);
    
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("최종 잔액: %d\n", balance); // 항상 500으로 출력됨

    pthread_mutex_destroy(&lock); // 뮤텍스 제거
    return 0;
}

✅ 실행 결과 (예측 가능)

최종 잔액: 500

🔹 락을 사용하면 한 번에 하나의 스레드만 balance를 수정할 수 있으므로 경쟁 상태가 발생하지 않음.
🔹 pthread_mutex_lock()을 사용하여 보호하고, 작업이 끝나면 반드시 pthread_mutex_unlock()으로 해제해야 함.


✅ 2. 스레드 간 순서 조정 - Condition Variable 사용

일부 경우에서는 Lock을 사용하지 않고 스레드 간 실행 순서를 조정하여 해결할 수도 있습니다.

🟢 해결 코드: Condition Variable 적용

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t lock;
pthread_cond_t cond;
int balance = 1000; 
int done = 0;

void* withdraw(void* arg) {
    pthread_mutex_lock(&lock);
    while (!done) pthread_cond_wait(&cond, &lock); // 🔄 특정 조건이 충족될 때까지 대기

    balance -= 500; 
    pthread_mutex_unlock(&lock);
    return NULL;
}

void* notify(void* arg) {
    pthread_mutex_lock(&lock);
    done = 1;
    pthread_cond_signal(&cond); // 🔔 대기 중인 스레드에게 신호 보내기
    pthread_mutex_unlock(&lock);
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_mutex_init(&lock, NULL);
    pthread_cond_init(&cond, NULL);

    pthread_create(&t1, NULL, withdraw, NULL);
    pthread_create(&t2, NULL, notify, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("최종 잔액: %d\n", balance);

    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&cond);
    return 0;
}

Condition Variable을 사용하면 특정 조건이 만족될 때만 스레드가 실행됨.
락 없이도 동기화가 가능하지만, 락과 함께 사용해야 안전함.


✅ 3. 원자적 연산(Atomic Operation) 활용

CPU가 제공하는 원자적 연산(Atomic Operation) 을 활용하면, 락을 사용하지 않고도 경쟁 상태를 방지할 수 있습니다.
C에서는 __sync_fetch_and_sub() 같은 GCC 내장 함수를 활용할 수 있습니다.

🟢 해결 코드: 원자적 연산 적용

#include <stdio.h>
#include <pthread.h>

int balance = 1000; // 공유 자원 (임계 영역)

void* withdraw(void* arg) {
    __sync_fetch_and_sub(&balance, 500); // ✅ 원자적 연산 적용 (Atomic Operation)
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, withdraw, NULL);
    pthread_create(&t2, NULL, withdraw, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("최종 잔액: %d\n", balance); // 항상 500이 유지됨
    return 0;
}

원자적 연산은 CPU 명령어 단위에서 실행되므로 락 없이도 동기화 가능.
하지만 모든 연산이 원자적으로 처리되지는 않으며, 복잡한 연산에는 여전히 락이 필요.


✅ 정리

  • 임계 영역(Critical Section) 은 여러 개의 스레드가 동시에 실행될 때 데이터 충돌이 발생할 가능성이 있는 코드 영역.
  • 해결 방법:
    1. Lock 사용 (Mutex, RWLock)
    2. Condition Variable로 실행 순서 제어
    3. 원자적 연산(Atomic Operation) 활용