
저자: 앨런 B. 다우니 (Allen B. Downey)
버전 2.2.1
The Little Book of Semaphores 다운로드 링크
세마포어는 유명한 괴짜 컴퓨터 과학자(!)인 Edsger Dijkstra가 발명했다. WOW
일반적인 정수와 비슷하지만, 정수와 다음과 같은 차이점이 있다.
1) 세마포어를 생성할 때는 어떤 정수든 초기값으로 설정할 수 있지만, 이후에는 증가, 감소만 가능하다.
2) 어떤 스레드가 세마포어를 감소시키고 그 결과가 음수가 되면, 그 스레드는 (스스로) 차단(block)이 되며 다른 스레드가 세마포어를 증가시킬 때까지 실행을 멈춘다.
3) 스레드가 세마포어를 증가시킬 때, 대기 중인 스레드가 있다면 그 중 하나가 unlock되어 실행을 재개한다.
참고) block = 운영체제의 스케줄러에게 해당 스레드는 현재 실행 불가능하다고 알리는 것이다. 차단된 스레드는 특정 조건을 만족해 해제되기 전까지 실행되지 않는다.
4) 세마포어의 값
의도적인 제약 조건을 부여함으로써, 프로그래머의 실수를 줄인다.(제약이 많은 만큼 안전)
구조가 깔끔하다
대부분의 시스템에서 효율적으로 구현 가능하며, 이식성과 성능이 좋다.
두 스레드가 동일한 변수 x에 접근한다고 가정해보겠습니다.
스레드 A는 x = x + 1,
스레드 B는 x = x + 2를 실행합니다.
이 코드는 어떤 결과를 초래할까요?
그리고 어떻게 해야 이 문제를 해결할 수 있을까요?
A가 먼저 실행: x = 1 + 1 = 2, 그 후 B가 실행: x = 2 + 2 = 4
B가 먼저 실행: x = 1 + 2 = 3, 그 후 A가 실행: x = 3 + 1 = 4
어떤 경우든 x = 4
1) 문제 상황(경쟁 조건 발생)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int x = 0; // 공유 변수
void* threadA(void* arg)
{
// 스레드 A: x = x + 1
int tmp = x; // x 읽기
usleep(10);
tmp = tmp + 1; // 연산
x = tmp; // 쓰기
return NULL;
}
void* threadB(void* arg)
{
// 스레드 B: x = x + 2
int tmp = x; // x 읽기
usleep(10);
tmp = tmp + 2; // 연산
x = tmp; // 쓰기
return NULL;
}
int main()
{
pthread_t t1, t2;
x = 1; // 초기값
pthread_create(&t1, NULL, threadA, NULL);
pthread_create(&t2, NULL, threadB, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("최종 결과: x = %d\n", x);
return 0;
}
이 코드의 정말 신기한 점:
usleep(10)을 걸기 전까지는 x = 4로 나왔는데, 저렇게 바꾼 순간부터 x = 2, 3, 4로 바뀌었다.
의미
usleep으로 인해 달라진게 무엇일까?
2) solution
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
int x = 0; // 공유 변수
sem_t mutex; // 세마포어 선언
void* threadA(void* arg)
{
sem_wait(&mutex); // 진입 (lock)
int tmp = x;
tmp = tmp + 1;
x = tmp;
sem_post(&mutex); // 해제 (unlock)
return NULL;
}
void* threadB(void* arg)
{
sem_wait(&mutex); // 진입 (lock)
int tmp = x;
tmp = tmp + 2;
x = tmp;
sem_post(&mutex); // 해제 (unlock)
return NULL;
}
int main()
{
pthread_t t1, t2;
x = 1; // 초기값
// 세마포어 초기화: 초기값 1 → 한 번에 하나의 스레드만 접근 가능
sem_init(&mutex, 0, 1);
pthread_create(&t1, NULL, threadA, NULL);
pthread_create(&t2, NULL, threadB, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("최종 결과: x = %d\n", x);
// 세마포어 제거
sem_destroy(&mutex);
return 0;
}
1) 문맥: CPU가 현재 실행 중인 작업에 대한 모든 상태
2) 프로세스 문맥 전환 vs 스레드 문맥 전환
프로세스 간 전환
스레드 간 전환(같은 프로세스)
멀티플렉스: 동시 접근 개수를 제한하는 세마포어 패턴
엘리베이터가 한 번에 최대 3명만 탈 수 있다고 가정해봅시다.
엘리베이터를 이용하려는 사람들을 스레드로 표현한다면,
동시에 3명까지만 입장 가능하게끔 코드를 작성해보세요.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define NUM_TREADS 5
sem_t elevator;
void *person(void *arg)
{
int id = *((int *)arg);
free(arg);
printf("사람 %d: 엘리베이터 타기 대기 중...\n", id);
sem_wait(&elevator); // 엘리베이터 타기
printf("사람 %d: 엘리베이터 탑승!\n", id);
sleep(1); // 엘리베이터 타고 이동 중
printf("사람 %d: 엘리베이터에서 내림!(나는 정상적으로 내려요)\n", id);
sem_post(&elevator);
return NULL;
}
int main()
{
pthread_t threads[NUM_TREADS];
sem_init(&elevator, 0, 2); // 엘리베이터 정원 2명
for (int i = 0; i < NUM_TREADS; i++)
{
int *id = malloc(sizeof(int));
*id = i + 1;
pthread_create(&threads[i], NULL, person, id);
usleep(200000);
}
for (int i = 0; i < NUM_TREADS; i++)
{
pthread_join(threads[i], NULL);
}
sem_destroy(&elevator);
return 0;
}

1) solution: 동시 접근은 허용하되, 제한된 수만 허용하고자 할 때 사용되는 일반적인 동기화 패턴
2) 일반화
3) 주의사항
지금까지 살펴본 동기화 패턴은 대부분 비공정한 방식이다. 예를 들어, 세마포어를 사용할 경우, 어떤 스레드가 wait()에 도달하면 언제 실행 재개될지는 보장되지 않는다. 결국, 어떤 스레드는 계속 기다리게 되는 문제가 발생할 수 있다.(이를 기아 starvation이라고 한다.)
이런 문제를 방지하기 위해, 스레드가 도착한 순서대로 처리하는 큐(queue)를 만들 수 있다.
여러 스레드가 어떤 자원(resource)에 접근하려고 대기할 때
도착한 순서대로 접근할 수 있도록 하려면 어떻게 해야 할까요?
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define NUM_THREADS 5
#define MAX_QUEUE 100
sem_t *queue[MAX_QUEUE];
int front = 0;
int rear = 0;
sem_t mutex;
void enqueue(sem_t *sem)
{
queue[rear] = sem;
rear = (rear + 1) % MAX_QUEUE;
}
sem_t *dequeue()
{
if (front == rear)
return NULL;
sem_t *sem = queue[front];
front = (front + 1 ) % MAX_QUEUE;
return sem;
}
void use_resource(int id)
{
printf("스레드 %d: 자원 사용 시작\n", id);
// sleep(1);
// 0~0.5초 랜덤 대기
usleep((rand() % 500000));
printf("스레드 %d: 자원 사용 종료\n", id);
}
void *thread_func(void *arg)
{
int id = *((int *)arg);
free(arg);
sem_t my_turn;
sem_init(&my_turn, 0, 0);
// 큐에 등록
sem_wait(&mutex);
int is_first = (front == rear); // 내가 첫번째?
enqueue(&my_turn);
sem_post(&mutex);
// 첫 번째가 아니라면 차례를 기다림
if (!is_first)
sem_wait(&my_turn);
// 자원 사용
use_resource(id);
// 다음 스레드에게 신호
sem_wait(&mutex);
sem_t *next = NULL;
dequeue(); // 나 제거
if (front != rear)
next = queue[front]; // 여기가 중요, 큐가 비어있지 않다면 그냥 조회
if (next)
sem_post(next);
// else
// {
// front = rear = 0; // 큐 비우기
// }
sem_post(&mutex);
sem_destroy(&my_turn);
return NULL;
}
int main()
{
pthread_t threads[NUM_THREADS];
sem_init(&mutex, 0, 1);
for (int i = 0; i < NUM_THREADS; i++)
{
int *id = malloc(sizeof(int));
*id = i + 1;
pthread_create(&threads[i], NULL, thread_func, id);
usleep(100000); // 0.1초 간격으로 생성
}
for (int i = 0; i < NUM_THREADS; i++)
pthread_join(threads[i], NULL);
sem_destroy(&mutex);
return 0;
}
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define THREADS 5
typedef struct s_ticket_lock
{
pthread_mutex_t lock;
pthread_cond_t cond;
int next_ticket;
int now_serving;
} t_ticket_lock;
void ticket_lock(t_ticket_lock *tl, int *my_ticket)
{
pthread_mutex_lock(&tl->lock);
*my_ticket = tl->next_ticket++;
while (tl->now_serving != *my_ticket)
pthread_cond_wait(&tl->cond, &tl->lock);
pthread_mutex_unlock(&tl->lock);
}
void ticket_unlock(t_ticket_lock *tl)
{
pthread_mutex_lock(&tl->lock);
tl->now_serving++;
pthread_cond_broadcast(&tl->cond);
pthread_mutex_unlock(&tl->lock);
}
void *worker(void *arg)
{
t_ticket_lock *tl;
int my_ticket;
tl = (t_ticket_lock *)arg;
ticket_lock(tl, &my_ticket);
printf("Thread got resource (ticket %d)\n", my_ticket);
usleep(100000); // 자원 사용 (0.1초)
ticket_unlock(tl);
return (NULL);
}
int main(void)
{
pthread_t t[THREADS];
t_ticket_lock tl;
int i;
pthread_mutex_init(&tl.lock, NULL);
pthread_cond_init(&tl.cond, NULL);
tl.next_ticket = 0;
tl.now_serving = 0;
for (i = 0; i < THREADS; i++)
pthread_create(&t[i], NULL, worker, &tl);
for (i = 0; i < THREADS; i++)
pthread_join(t[i], NULL);
pthread_mutex_destroy(&tl.lock);
pthread_cond_destroy(&tl.cond);
return (0);
}
| 구분 | 세마포어 기반 큐 (책 해법) | 티켓락 기반 큐 (조건변수/스핀락) |
|---|---|---|
| 순서 보장 | 큐 배열에 my_turn 세마포어를 등록하여, 앞선 스레드가 끝날 때 signal() → FIFO 보장 | next_ticket을 발급받고, now_serving과 비교 → FIFO 보장 |
| 대기 방식 | 세마포어 wait() 호출 시 → 스레드가 블록(block) 상태로 전환되어 CPU 점유 안 함 | while (my_ticket != now_serving) 형태의 busy wait (스핀)으로 CPU 점유 |
| 효율성 | OS가 스케줄링 관리 → CPU 자원 효율적 | 스레드가 계속 돌며 대기 → CPU 낭비 가능 |
| 공정성(fairness) | 도착 순서 그대로 실행 → starvation 없음 | ticket 번호 순서대로 실행 → starvation 없음 |
| 구현 난이도 | 큐 자료구조 + 세마포어 관리 필요 → 구현 복잡 | ticket 번호 증가/비교만으로 동작 → 구현 간결 |
| 사용 환경 | 사용자 공간 프로그램에 적합 (pthread, POSIX 환경 등) | 커널/멀티코어 저수준 환경에서 주로 사용 |
| 확장성 | 스레드 수가 늘어도 안정적 | 스레드가 많아질수록 busy wait 부작용 커짐 |
| 대표적 키워드 | sem_wait, sem_post, 큐 자료구조 | next_ticket, now_serving, spin, fairness |

https://pages.cs.wisc.edu/~remzi/OSTEP/Korean/
