여러 프로세스와 스레드가 동시에 실행되는 환경에서 각각이 서로 영향을 주고받지 않고 올바르게 동작하도록 보장하는 것이 동기화다. 협력하여 공동의 목적을 수행해야 하는 프로세스들은 실행 순서와 자원 접근의 일관성을 유지해야 한다. 운영체제는 이를 위해 다양한 동기화 기법을 제공한다.
동기화는 정보 통신 분야에서 ‘여러 작업의 수행 시기를 맞추는 것’을 의미함. 운영체제에서는 다음 두 가지 관점에서 가장 많이 활용된다.
| 구분 | 설명 |
|---|---|
| 실행 순서 제어 | 여러 프로세스를 올바른 순서대로 수행하도록 보장 |
| 상호 배제 | 동시에 접근하면 안 되는 공유 자원에 단 하나의 실행 흐름만 접근하도록 보장 |
프로세스뿐 아니라 스레드 역시 실행의 흐름을 가지고 있으므로 모두 동기화 대상이 된다.
프로세스가 어떤 동작을 수행하기 위해서는 반드시 특정 순서를 따라야 한다. 잘못된 순서로 실행되면 결과가 달라지는 대표 사례가 Read-Write 문제다.
아래와 같은 공유 변수 x가 있다고 가정한다.
초기값 x = 10
두 개의 프로세스 P1, P2가 다음 순서로 실행되어야 한다.
하지만 동기화 없이 실행되면 다음과 같은 문제가 발생함.
P1: x를 읽음 → 10
P2: x를 읽음 → 10
P1: x = 11 저장
P2: x = 12 저장
정답은 13이어야 하지만 12가 됨. 순서가 보장되지 않아 발생하는 오류다. 이를 순서 동기화로 해결한다.
수많은 프로세스가 공유 자원에 접근할 때 문제가 발생할 수 있다. 이러한 문제가 발생하는 구역을 임계 구역(critical section)이라고 한다.
| 용어 | 설명 |
|---|---|
| 공유 자원 | 전역 변수, 파일, I/O 장치, 보조기억장치 등 |
| 임계 구역 | 동시에 접근하면 오류를 일으키는 코드 영역 |
| 레이스 컨디션 | 동시에 실행되어 실행 결과가 실행 순서에 따라 달라지는 상황 |
컴퓨터는 고급 언어 코드를 저급 언어로 변환한 뒤 실행한다. 중간에 인터럽트가 발생하거나 문맥 교환이 발생하면 한 프로세스가 하던 작업을 다른 프로세스가 끼어들어 변경할 수 있고, 이로 인해 레이스 컨디션이 발생한다.
가장 대표적인 동기화 문제는 버퍼를 사이에 둔 생산자와 소비자의 문제다.
문제 상황 예시
| 문제 | 설명 |
|---|---|
| 버퍼 오버플로 | 생산자가 너무 빨리 데이터를 넣을 경우 |
| 버퍼 언더플로 | 소비자가 너무 빨리 데이터를 꺼낼 경우 |
| 레이스 컨디션 | 서로 동시에 버퍼 포인터를 갱신하는 경우 |
운영체제는 이러한 문제를 해결하기 위해 동기화 도구를 제공한다.
대표적인 동기화 도구는 다음 세 가지다.
| 도구 | 특징 | 장점 | 단점 |
|---|---|---|---|
| 뮤텍스 락(Mutex Lock) | 임계 구역에 하나만 들어가도록 잠금 기반 제어 | 구현이 단순 | 바쁜 대기(스핀)로 CPU 낭비 |
| 세마포어(Semaphore) | 정수 값으로 접근 가능 개수를 제어 | 다중 접근 가능 | 자원 고갈·교착 문제 관리가 비교적 어려움 |
| 모니터(Monitor) | 언어 차원에서 제공하는 동기화 구조 | 구현이 간결 | OS보다는 언어 구현체 의존 |
임계 구역을 보호하기 위해 lock과 unlock을 사용한다.
lock(mutex)
임계 구역
unlock(mutex)
뮤텍스는 한 번에 하나만 임계 구역에 들어갈 수 있도록 보장한다.
다만, lock이 해제될 때까지 계속 반복 검사하는 바쁜 대기(spin lock) 방식이 사용될 수 있어 CPU를 소모함.
세마포어는 정수 값을 가지는 동기화 도구다.
세마포어는 세마포와 동일한 개념이며, Semaphore(sema + phore)는 원래 Dijkstra가 정의한 동기화 메커니즘을 말한다.
| 종류 | 값 | 설명 |
|---|---|---|
| 이진 세마포어(binary semaphore) | 0 또는 1 | 뮤텍스와 유사. 한 번에 하나만 접근 가능 |
| 카운팅 세마포어(counting semaphore) | 0 이상 | 특정 자원의 최대 접근 가능 횟수를 제한 |
P() or wait(): 값 감소 → 0 미만이면 대기
V() or signal(): 값 증가 → 대기 중인 프로세스 깨움
| 세마포어 | 초기값 | 의미 |
|---|---|---|
| empty | 버퍼 크기 | 남은 공간 수 |
| full | 0 | 채워진 데이터 수 |
| mutex | 1 | 임계 구역 보호 |
생산자와 소비자 모두 empty, full, mutex를 조합하여 경합 없이 버퍼를 공유한다.
모니터는 상호 배제와 조건 변수를 언어 수준에서 관리하는 구조다.
Java, C#, Go 등에서 제공하는 동기화 구문이 모니터 기반이다.
질문: 세마포는 세마포어인가?
답변: 동일한 개념이다.
Semaphore를 한국어로 세마포 또는 세마포어라고 부른다. 단순 표기 차이다.
대표적인 세마포어 기반 동기화 도구:
sem_init, sem_wait, sem_post)threading.Semaphore예시(POSIX):
sem_t sem;
sem_init(&sem, 0, 1); // 초기값 1 (이진 세마포어)
sem_wait(&sem); // P()
/* 임계 구역 */
sem_post(&sem); // V()
단일 자원 보호뿐 아니라, 버퍼 관리, 네트워크 연결 수 제한 등 다양한 자원 관리에 활용된다.
RabbitMQ로 레이스 컨디션을 방지할 수 있을까? 해당 기능이 세마포어일까?
RabbitMQ는 동기화 도구가 아니기 때문이며, 임계 구역 보호 능력이 없다.
하지만
특정 상황에서는 "우연히" 레이스 컨디션을 줄이는 효과가 발생할 수는 있다.
→ 하지만 이것도 진짜 해결이 아니라 "부작용적 완화"일 뿐이다.
아래에서 왜 직접 해결이 불가능한지, 어떤 경우에 간접 완화가 가능한지 정확히 분석해줄게.
레이스 컨디션은 공유 자원에 동시에 접근할 때 발생하는 문제이다.
예:
RabbitMQ는 다음을 제공할 뿐이다:
즉 RabbitMQ는 "작업을 분배하는 도구"일 뿐,
DB row lock을 걸지도 않고
메모리 접근을 보호하지도 않고
파일 접근 동기화도 하지 않는다.
RabbitMQ는 임계 구역 개념이 없다.
따라서 레이스 컨디션을 해결할 수 없다.
RabbitMQ의 큐 특성 때문에
여러 Worker가 동시에 같은 작업을 처리하는 일이 줄어들기 때문이다.
진입 작업이 메시지 큐에 하나씩 들어오고
Worker들이 큐에서 하나씩 가져간다.
즉 "작업(Task)에 대한 레이스"는 줄어든다.
이 과정은 다음과 같다:
Producer → Queue → Worker1
Worker2
Worker3
그래서 사람들은 종종:
“RabbitMQ가 concurrency 문제를 해결해주는 것 같다?”
라고 느끼지만,
이는 작업 단위의 충돌 방지일 뿐, 자원 자체의 충돌은 해결하지 못한다.
예시를 보자:
Worker 1:
SELECT balance FROM account WHERE id=10; -- retrieves 100
balance = balance - 50 → 50
Worker 2:
(Worker 1이 commit하기 전에 거의 동시에)
SELECT balance FROM account WHERE id=10; -- retrieves 100
balance = balance - 30 → 70
결과:
RabbitMQ는 이런 상황을 절대 방지할 수 없다.
왜냐하면 DB 자원을 보호하지 않기 때문이다.
예를 들면:
account.10 → queue.10 → worker.10 (1개)
account.20 → queue.20 → worker.20 (1개)
→ 이러면 같은 계좌에 대한 작업은 항상 순차적으로 처리
→ 사실상 "자원별 Mutex" 역할
하지만 이건 RabbitMQ 자체 기능으로 해결한 게 아니라
아키텍처를 그렇게 설계한 결과물이다.
예:
RabbitMQ + Redlock(분산 락) 구조:
Consumer → Redis Lock 획득 → 작업 수행 → 락 해제
이 경우에는 실제로 레이스 컨디션을 해결할 수 있다.
하지만 해결하는 것은 "Redis Lock"이다.
RabbitMQ가 아니다.
하지만 이것은 간접 효과일 뿐이며
“공유 자원”에 대한 레이스는 여전히 발생한다.
RabbitMQ는 이들 중 어느 것도 대체할 수 없다.