프로세스 동기화

msgo·2025년 12월 10일

프로세스 동기화

여러 프로세스와 스레드가 동시에 실행되는 환경에서 각각이 서로 영향을 주고받지 않고 올바르게 동작하도록 보장하는 것이 동기화다. 협력하여 공동의 목적을 수행해야 하는 프로세스들은 실행 순서와 자원 접근의 일관성을 유지해야 한다. 운영체제는 이를 위해 다양한 동기화 기법을 제공한다.

동기화 개념

동기화는 정보 통신 분야에서 ‘여러 작업의 수행 시기를 맞추는 것’을 의미함. 운영체제에서는 다음 두 가지 관점에서 가장 많이 활용된다.

구분설명
실행 순서 제어여러 프로세스를 올바른 순서대로 수행하도록 보장
상호 배제동시에 접근하면 안 되는 공유 자원에 단 하나의 실행 흐름만 접근하도록 보장

프로세스뿐 아니라 스레드 역시 실행의 흐름을 가지고 있으므로 모두 동기화 대상이 된다.


실행 순서 제어

프로세스가 어떤 동작을 수행하기 위해서는 반드시 특정 순서를 따라야 한다. 잘못된 순서로 실행되면 결과가 달라지는 대표 사례가 Read-Write 문제다.

Read-Write 예시

아래와 같은 공유 변수 x가 있다고 가정한다.

초기값 x = 10

두 개의 프로세스 P1, P2가 다음 순서로 실행되어야 한다.

  • P1: x를 읽고 1 증가시키기
  • P2: x에 2를 더하기

하지만 동기화 없이 실행되면 다음과 같은 문제가 발생함.

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)

세마포어는 정수 값을 가지는 동기화 도구다.
세마포어는 세마포와 동일한 개념이며, Semaphore(sema + phore)는 원래 Dijkstra가 정의한 동기화 메커니즘을 말한다.

세마포어 종류

종류설명
이진 세마포어(binary semaphore)0 또는 1뮤텍스와 유사. 한 번에 하나만 접근 가능
카운팅 세마포어(counting semaphore)0 이상특정 자원의 최대 접근 가능 횟수를 제한

세마포어 연산

P() or wait(): 값 감소 → 0 미만이면 대기
V() or signal(): 값 증가 → 대기 중인 프로세스 깨움

생산자-소비자 문제에서의 세마포어 예시

세마포어초기값의미
empty버퍼 크기남은 공간 수
full0채워진 데이터 수
mutex1임계 구역 보호

생산자와 소비자 모두 empty, full, mutex를 조합하여 경합 없이 버퍼를 공유한다.


모니터(Monitor)

모니터는 상호 배제와 조건 변수를 언어 수준에서 관리하는 구조다.

  • 프로세스는 모니터 내부의 메서드 중 하나에만 진입 가능
  • 조건 변수(condition variable)로 wait, signal 제공
  • 사용자가 lock, unlock을 직접 작성할 필요 없음

Java, C#, Go 등에서 제공하는 동기화 구문이 모니터 기반이다.


세마포어는 세마포어인가?

질문: 세마포는 세마포어인가?

답변: 동일한 개념이다.
Semaphore를 한국어로 세마포 또는 세마포어라고 부른다. 단순 표기 차이다.


세마포어 도구 예시

대표적인 세마포어 기반 동기화 도구:

  1. POSIX 세마포어 (sem_init, sem_wait, sem_post)
  2. System V 세마포어
  3. Windows Semaphore
  4. 파이썬 threading.Semaphore
  5. 리눅스 커널 세마포어

예시(POSIX):

sem_t sem;

sem_init(&sem, 0, 1);  // 초기값 1 (이진 세마포어)

sem_wait(&sem);        // P()
/* 임계 구역 */
sem_post(&sem);        // V()

단일 자원 보호뿐 아니라, 버퍼 관리, 네트워크 연결 수 제한 등 다양한 자원 관리에 활용된다.


RabbitMQ로 레이스 컨디션을 방지할 수 있을까? 해당 기능이 세마포어일까?

RabbitMQ로 레이스 컨디션을 방지할 수 없다(직접적으로는).

RabbitMQ는 동기화 도구가 아니기 때문이며, 임계 구역 보호 능력이 없다.

하지만
특정 상황에서는 "우연히" 레이스 컨디션을 줄이는 효과가 발생할 수는 있다.
→ 하지만 이것도 진짜 해결이 아니라 "부작용적 완화"일 뿐이다.

아래에서 왜 직접 해결이 불가능한지, 어떤 경우에 간접 완화가 가능한지 정확히 분석해줄게.


1. RabbitMQ로 레이스 컨디션을 해결할 수 없는 이유

레이스 컨디션은 공유 자원에 동시에 접근할 때 발생하는 문제이다.
예:

  • 같은 DB 레코드를 두 요청이 동시에 수정
  • 동일 파일에 두 프로세스가 동시에 write
  • 메모리 변수의 증감 연산이 동시에 실행

RabbitMQ는 다음을 제공할 뿐이다:

  • 메시지 큐(Queue)
  • 라우팅
  • 메시지 전달 보장
  • 비동기 작업 처리

즉 RabbitMQ는 "작업을 분배하는 도구"일 뿐,

DB row lock을 걸지도 않고
메모리 접근을 보호하지도 않고
파일 접근 동기화도 하지 않는다.

RabbitMQ는 임계 구역 개념이 없다.
따라서 레이스 컨디션을 해결할 수 없다.


2. 하지만 "일부 상황에서" 레이스 컨디션이 줄어드는 이유

RabbitMQ의 큐 특성 때문에
여러 Worker가 동시에 같은 작업을 처리하는 일이 줄어들기 때문이다.

예시:

진입 작업이 메시지 큐에 하나씩 들어오고
Worker들이 큐에서 하나씩 가져간다.

즉 "작업(Task)에 대한 레이스"는 줄어든다.

이 과정은 다음과 같다:

Producer → Queue → Worker1
                   Worker2
                   Worker3
  • 큐에 동일한 작업이 두 번 들어가지 않는다면
    → 여러 Worker가 같은 작업을 동시에 수행할 일이 없음
  • 메시지는 한 Worker만 가져가기 때문에
    → 메시지 단위 "독점 처리"가 가능해 보인다

그래서 사람들은 종종:

“RabbitMQ가 concurrency 문제를 해결해주는 것 같다?”

라고 느끼지만,

이는 작업 단위의 충돌 방지일 뿐, 자원 자체의 충돌은 해결하지 못한다.

예시를 보자:

예: 여러 Worker가 같은 DB 레코드를 바로 이어서 수정하는 경우

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

결과:

  • Worker 1이 commit → balance = 50
  • Worker 2가 commit → balance = 70 ← 잘못된 값

RabbitMQ는 이런 상황을 절대 방지할 수 없다.

왜냐하면 DB 자원을 보호하지 않기 때문이다.


3. RabbitMQ로 간접적으로 레이스 컨디션을 줄일 수 있는 패턴

① 특정 자원에 대한 메시지를 Key별 Queue로 분산하는 전략

예를 들면:

  • 계좌 ID를 routing key로 사용하고
  • 계좌별로 queue를 하나씩 만들어서
  • 각 queue에 Worker 1개만 배정
account.10 → queue.10 → worker.10 (1개)
account.20 → queue.20 → worker.20 (1개)

→ 이러면 같은 계좌에 대한 작업은 항상 순차적으로 처리
→ 사실상 "자원별 Mutex" 역할

하지만 이건 RabbitMQ 자체 기능으로 해결한 게 아니라
아키텍처를 그렇게 설계한 결과물이다.

② 메시지를 직렬화(serialize) 처리하도록 Worker를 단일화

예:

  • Worker를 1개만 둔다 → 항상 직렬 처리
    (하지만 성능 매우 낮음)
③ DB Lock / Redis Lock / ETCD Lock을 결합

RabbitMQ + Redlock(분산 락) 구조:

Consumer → Redis Lock 획득 → 작업 수행 → 락 해제

이 경우에는 실제로 레이스 컨디션을 해결할 수 있다.
하지만 해결하는 것은 "Redis Lock"이다.
RabbitMQ가 아니다.


RabbitMQ 자체로는 레이스 컨디션을 절대 해결할 수 없다.

  • 임계 구역 보호 기능 없음
  • 자원 접근을 직렬화하지 않음
  • OS나 DB 수준 동기화 제공 X
✔ “작업(Task)”의 레이스는 줄어들 수 있다.

하지만 이것은 간접 효과일 뿐이며
“공유 자원”에 대한 레이스는 여전히 발생한다.

진짜 레이스 컨디션 방지는 락(Lock)이 필요하다.
  • DB row-level lock
  • Redis distributed lock
  • Zookeeper / ETCD lock
  • OS mutex/semaphore

RabbitMQ는 이들 중 어느 것도 대체할 수 없다.


0개의 댓글