STUDY/정글

[csapp] 세마포어로 쓰레드 동기화하기 (12.5)

Nobb 2025. 11. 3. 21:29

# 개요 ----

"세마포어" 사용해서 스레드 기반 동시 프로그래밍의 "경쟁 상태(race condition)" 문제 해결하기

 

 - 일반적으로 os가 쓰레드 정확한 순서의 선택 여부를 예측할 수 없음

 

 

 

# 용어 정리 ----

 * 세마포어  : 공유자원의 사용 가능 개수를 나타내는 정수 변수 (전역)

    - 공유자원에 대한 접근

    - 스레드 간 실행순서 동기화

 * 경쟁 상태  : 여러 스레드/프로세스가 공유 데이터를 R/W 작업 할 때,

                    실행 순서에 따라서 잘못된 값을 읽거나 쓰게 되는 상황

 

 * 임계구역 : 한 번에 오직 하나만 접근 가능한 구역

 

# 세마포어 vs 뮤텍스 ----

 * 둘다 한 시점에 오직 하나의 스레드만 공유자원에 들어가도록 보장

 

/ 세마포어   (from. 다익스트라)

 : 일종의 정수 카운터변수 (0->대기, P->-1, V->+1) 

    > 초기값: 자원 M개일때 M , 상호배제용일때 1 (0또는 1 --> 뮤텍스처럼 동작)

    > 용도: 자원 수 제어, 동기화

 

/ 뮤텍스

 : 락 객체 (초기값: 1 (locked / unlocked 둘 중 하나))   // 순수 lock 객체

    - 용도: 상호배제 전용

    - 락 소유자만 unlock 가능

 

 

# 세마포어 기본 원리 ----

 : 오직 2가지 "원자적 연산"을 통해서만 접근할 수 있는 정수변수 S 로 구성됨.

    - 이 연산들이 진행되는 동안에는 절대 다른 스레드에 의해 방해받지 않음

    - 기본 작동 원리는 Mutex(상호 배제 알고리즘) 기반.

1) P 연산 (자원 사용하기 전에 획득 요청 -> S가 0보다 작아지면 대기)

    S = S-1;
    
2) V 연산 (자원 사용 마친 후 반납 -> 대기중인 스레드 있음 깨우기)

    S = S+1;

 

# 세마포어 정수 S의 의미 -----

    * 양수 _ 현재 사용 가능한 공유 자원의 개수

    * 0 _ 사용 가능한 자원이 없음

    * 음수 _ 현재 자원 기다리며 대기중인 스레드 개수

 

    * 보통 초기값: 1

 

 

# 실제 사용 방법

    #include <sempaphore.h>

    sem_t mutex; //전역변수 (모든 스레드가 접근할 수 있어야함, 보호하려는 공유변수와 같은 스코프에 존재해야함)

 

# problem ====
for (i=0; i<niters; i++)
	cnt++;
>>>>>> 
T1: L(cnt=0)
T2: L(cnt=0)
T1: U(1)
T2: U(1)
T1: S(1)
T2: S(1)
결과: cnt = 1   (증가가 하나 날아감!)
===========


# sem ver. ====
sem_t mutex;  // 세마포어 선언 (전역 변수)
sem_init(&mutex, 0, 1); // 초기값 1 → binary semaphore (즉, 뮤텍스처럼)

void *thread(void *vargp) {
    long i, niters = *((long *)vargp);

    for (i = 0; i < niters; i++) {
        P(&mutex);    // 🔒 세마포어 잠금 (임계 구역 진입 전)  //s:0
        cnt++;        // 🔥 공유 변수 접근 (critical section)
        V(&mutex);    // 🔓 세마포어 해제 (임계 구역 빠져나올 때)  //s:1
    }
    return NULL;
}

 

 

# 12.5.1-  진행그래프 ----

 : 여러 스레드가 동시에 실행될 때, 그 진행상태를 시각적으로 표현하는 도구

    - 스레드들의 동시 실행, 잠재적 경쟁상태 시각적 이해 도구

 

==== 공유 변수에 Write 접근 시,

LOAD -> UPDATE -> STORE

이 과정을 겪는데, 이 과정에서 다른 스레드랑 겹치게 진행되면 꼬임.

 

> 각 공유자원에 세마포어(s) 연결해 씀 (보통 하나의 독립된 공유데이터묶음마다 1개씩)

 

 

# 세마포어로 공유자원 스케쥴링 ======

 * 세마포어 역할: 위에서처럼 상호 배제 제공도 하지만,

    공유자원으로의 접근을스케쥴링도 함

 >>> 그 예시: 생산자-소비자 , reader-writer

 

# 생산자-소비자 ====

* 생산자: 물건을 만들어서 버퍼에 넣는 스레드

* 소비자: 버퍼에서 물건을 꺼내 쓰는 스레드

 

>> 동시 접근하면 엉망됨 (경쟁상태)

 

  • 버퍼가 꽉 찼는데 생산자가 또 넣으면?
    → 버퍼 overflow 🚫
  • 버퍼가 비었는데 소비자가 꺼내려 하면?
    → underflow 🚫
  • 둘이 동시에 버퍼 수정하면?
    → race condition 🔥

 

 

#define N 5              // 버퍼 크기
int buffer[N];           // 공유 버퍼
int in = 0, out = 0;     // 생산, 소비 위치 인덱스

sem_t mutex;             // 상호배제용 binary 세마포어
sem_t empty;             // 남은 빈칸 수
sem_t full;              // 찬 칸 수

void *producer(void *arg) {
    int item;
    while (1) {
        item = produce_item();       // 🍞 물건 하나 만듦
        P(&empty);                   // 빈칸이 있을 때까지 대기
        P(&mutex);                   // 버퍼 잠금
        buffer[in] = item;           // 버퍼에 넣기
        in = (in + 1) % N;           // 원형 버퍼 인덱스 갱신
        V(&mutex);                   // 버퍼 잠금 해제
        V(&full);                    // 물건 하나 늘었음 알림
    }
}

void *consumer(void *arg) {
    int item;
    while (1) {
        P(&full);                    // 찬 칸이 있을 때까지 대기
        P(&mutex);                   // 버퍼 잠금
        item = buffer[out];          // 버퍼에서 꺼내기
        out = (out + 1) % N;         // 인덱스 갱신
        V(&mutex);                   // 잠금 해제
        V(&empty);                   // 빈칸 하나 생겼다고 알림
        consume_item(item);          // 🍴 소비
    }
}

>> 공유버퍼 두고 싸우는 두 스레드를 질서있게 만들어주는 empty, full, mutex 3 세마포어 동작

 ----> empty: 빈자리 통제 / full: 물건 통제 / mutex: 손 안겹치게

 

>>>> 이렇게 buffer,in,out,empty,full,mutex 이런걸 한번에 쓰는게 sbuf_t 구조체

 

#include "csapp.h"
#include "sbuf.h"

void sbuf_init(sbuf_t *sp, int n) {
    sp->buf = Calloc(n, sizeof(int));  // 버퍼 메모리 동적할당
    sp->n = n;                         // 버퍼 크기
    sp->front = sp->rear = 0;          // front/rear 인덱스 초기화
    Sem_init(&sp->mutex, 0, 1);        // 뮤텍스 세마포어 (상호배제)
    Sem_init(&sp->slots, 0, n);        // 남은 슬롯(empty) 세마포어
    Sem_init(&sp->items, 0, 0);        // 채워진 슬롯(full) 세마포어
}

void sbuf_deinit(sbuf_t *sp) {
    Free(sp->buf);                     // 버퍼 메모리 해제
}

void sbuf_insert(sbuf_t *sp, int item) {
    P(&sp->slots);                     // 🔒 남은 공간 확보 (empty--)
    P(&sp->mutex);                     // 🔒 뮤텍스 잠금
    sp->buf[(++sp->rear) % (sp->n)] = item;  // 아이템 넣기
    V(&sp->mutex);                     // 🔓 뮤텍스 해제
    V(&sp->items);                     // 🔓 아이템 하나 추가 (full++)
}

int sbuf_remove(sbuf_t *sp) {
    int item;
    P(&sp->items);                     // 🔒 아이템 존재 대기 (full--)
    P(&sp->mutex);                     // 🔒 버퍼 잠금
    item = sp->buf[(++sp->front) % (sp->n)]; // 아이템 꺼내기
    V(&sp->mutex);                     // 🔓 버퍼 잠금 해제
    V(&sp->slots);                     // 🔓 빈 공간 증가 (empty++)
    return item;
}

====
typedef struct {
    int *buf;         // 공유 버퍼 배열
    int n;            // 버퍼 크기
    int front;        // 소비자 포인터
    int rear;         // 생산자 포인터
    sem_t mutex;      // 상호배제용 binary 세마포어
    sem_t slots;      // empty count
    sem_t items;      // full count
} sbuf_t;

>>> 이거 이용해 생산자-소비자 나타내면,

sbuf_t sbuf;

int main() {
    sbuf_init(&sbuf, 16);    // 버퍼 크기 16
    pthread_create(..., producer, NULL);
    pthread_create(..., consumer, NULL);
}

void *producer(void *arg) {
    while (1) {
        int item = produce_item();
        sbuf_insert(&sbuf, item);   // 동기화된 삽입 함수
    }
}

void *consumer(void *arg) {
    while (1) {
        int item = sbuf_remove(&sbuf); // 동기화된 제거 함수
        consume_item(item);
    }
}

 

 

 

 

reader-writer

🎬 1️⃣ 문제의 시나리오

  • 여러 reader(읽는 사람)
  • 여러 writer(쓰는 사람)
    하나의 공유 자원(예: 파일, 데이터베이스, 메모리) 을 동시에 접근하려고 해요.

⚠️ 2️⃣ 규칙 (이 문제의 핵심 규율)

1️⃣ 여러 reader들은 동시에 읽어도 괜찮아요.

읽기는 데이터 안 바꾸니까, conflict 없음.

2️⃣ 하지만 writer가 쓸 땐 혼자만 들어와야 해요.

쓰는 동안 누가 읽거나 또 쓰면, 데이터 일관성 깨져요 💀


즉 👇

상황허용?이유
여러 reader ✅ O 읽기만 함
여러 writer ❌ X 데이터 망가짐
reader + writer 동시에 ❌ X writer가 변경 중일 수 있음

🧩 3️⃣ 이걸 세마포어로 제어하려면?

보통 이렇게 2가지 자원이 필요해요 👇

세마포어초기값역할
mutex 1 readcnt (읽는 사람 수) 보호
w 1 writer(또는 reader 전체) 접근 제어

그리고 공유 변수 하나 추가 👇

 
int readcnt = 0; // 현재 읽는 중인 reader 수
 
 
>>읽는 놈들(readers)은 많아도 돼요,
하지만 쓰는 놈(writers)은 한 놈씩 차례로.

 

'STUDY > 정글' 카테고리의 다른 글

[네트워크] TCP 소켓 통신 구조 정리  (0) 2025.11.05
[웹서버] echo 서버 구현  (0) 2025.11.05
[TIL] 포인터 문제풀이(2) - G2G  (0) 2025.10.15
[정글] W5 퀴즈 (주요 내용 정리)  (0) 2025.10.14
[C] 포인터 문제풀이  (0) 2025.10.11