동시성

뮤텍스, 세마포어, 데드락, 원자 연산

뮤텍스는 한 번에 한 스레드만 진입시키고, 세마포어는 N개까지 허용한다. 락을 잘못 쓰면 두 스레드가 서로의 락을 기다리며 영원히 멈추는 데드락이 발생하며, 4가지 조건(상호 배제, 점유 대기, 비선점, 순환 대기)을 모두 만족해야 생긴다. 원자 연산은 하드웨어 차원에서 읽기-수정-쓰기를 한 덩어리로 처리해 락 없이 동시성을 다룬다.

Concurrency

개념

여러 스레드가 같은 자원에 동시에 접근하면 race condition이 생긴다. 이를 제어하는 도구가 동기화 장치이며, 대표적으로 뮤텍스, 세마포어, 조건 변수가 있다. 락 없이 하드웨어 원자 연산으로 처리하는 접근도 있다.

뮤텍스

Mutual Exclusion(상호 배제)의 줄임말. 한 번에 한 스레드만 진입할 수 있다. 화장실 열쇠가 1개인 것과 같다.

lock = Lock()
with lock:
    count += 1  # 여기 한 스레드만 진입

세마포어

뮤텍스의 일반화 버전. 한 번에 N개 스레드까지 동시 진입을 허용한다. 주차장에 자리가 N개인 것과 같다.

sem = Semaphore(3)
with sem:
    # 동시에 3 스레드까지 진입

DB 커넥션 풀이 대표적인 사용처다. 커넥션이 10개면 세마포어를 10으로 설정해서 동시에 10개까지만 DB에 접근하게 하고, 11번째 요청은 커넥션이 반환될 때까지 대기한다.

데드락

두 스레드가 서로 상대방이 가진 락을 기다리면서 영원히 멈추는 상태다.

스레드 1: 락A 잡음 → 락B 필요 → B를 스레드 2가 들고 있음 → 대기
스레드 2: 락B 잡음 → 락A 필요 → A를 스레드 1이 들고 있음 → 대기

데드락이 발생하려면 4가지 조건이 동시에 만족해야 한다.

  1. 상호 배제 — 락은 한 번에 하나만 잡을 수 있음
  2. 점유 대기 — 락 하나 잡은 채로 다른 락을 기다림
  3. 비선점 — 남이 가진 락을 강제로 뺏을 수 없음
  4. 순환 대기 — A→B→A처럼 서로 꼬리를 물며 기다림

4개 중 하나만 깨면 데드락이 안 생긴다. 가장 쉬운 방법은 락을 항상 같은 순서로 잡는 것이다. "항상 A 먼저, B 다음" 규칙을 정하면 순환 대기가 깨진다.

조건 변수

락을 잡고 들어왔는데 아직 할 일이 없는 경우에 쓴다. "조건이 만족될 때까지 락을 놓고 잠들게, 조건 되면 깨워줘"하는 장치다.

queue = []
lock = Lock()
cond = Condition(lock)

# 소비자
with cond:
    while not queue:
        cond.wait()      # 락 놓고 잠듦 → 생산자가 넣을 수 있게 됨
    item = queue.pop()

# 생산자
with cond:
    queue.append(item)
    cond.notify()        # 소비자 깨움

wait()가 락을 놓고 잠들고, notify()가 깨워주면서 락을 다시 잡는 구조다. 소비자가 락을 잡은 채로 대기하면 생산자가 큐에 넣을 수 없으니, 락을 놓고 자는 게 핵심이다.

원자 연산과 락-프리

count += 1이 실제로는 읽기 → 더하기 → 쓰기 3단계라서 race condition이 생긴다. 락으로 막을 수 있지만 다른 스레드를 대기시키니 느리다.

원자 연산(atomic operation)은 하드웨어 차원에서 읽기-수정-쓰기를 한 덩어리로 실행한다. 중간에 끊기지 않아서 락 없이도 안전하다.

락-프리(lock-free) 자료구조는 원자 연산을 활용해 락 없이 동시 접근을 처리한다. 대기가 없어서 빠르지만 구현이 어렵다. 직접 만들 일은 거의 없고, Java의 AtomicInteger나 Go의 sync/atomic 같은 라이브러리를 쓴다.

더 보기

sunshinemoon · 2026