C
운영체제/동기화/Lesson 04

동시성 — 락·데드락·동기/비동기·블로킹/논블로킹

60분·theory

동시성 — 락·데드락·동기/비동기·블로킹/논블로킹

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ Mutex vs Semaphore vs Monitor
  • ✅ Race Condition + Deadlock 4조건
  • ✅ 동기 vs 비동기 + 블로킹 vs 논블로킹 4조합

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

Race Condition — 동시 수정의 비극

한 줄: 두 스레드가 같은 데이터를 동시 수정 → 결과 예측 불가.

예시:

code
초기: counter = 0
Thread A: counter = counter + 1    (1)
Thread B: counter = counter + 1    (2)
기대: counter = 2
실제: counter = 1 (가능!)

이유: counter + 13개 명령:
1. read counter → register
2. register + 1
3. register → counter

A 가 1·2 후 B 가 1 하면 → 둘 다 0 읽고 +1 한 결과 1 만 저장.

해결:

방법용도
Mutex (Lock)한 번에 하나만 진입. 가장 흔함
SemaphoreN 개 허용 (예: DB 연결 풀)
Atomic단일 변수 (CAS·AtomicInteger) — 락 없음, 빠름
Channel/Actor메시지 패싱 (Go·Erlang) — 데이터 공유 회피
Immutable불변 데이터 (Rust·함수형) — race 자체 없음

Race 흔한 함정:

  • ❌ 디버거 켜면 사라지는 버그 (Heisenbug) — 디버거가 타이밍 바꿈
  • ❌ "내 컴퓨터에선 잘 됨" — CPU 코어 수·부하 다름
  • ✅ 항상 동시성 테스트 (-race 플래그·ThreadSanitizer)

Mutex — 가장 흔한 락

Mutex (MUTual EXclusion): 한 번에 하나의 스레드만 임계 구역(critical section) 진입.

사용 흐름:

python
import threading
lock = threading.Lock()
counter = 0

def increment():
    global counter
    with lock:           # acquire
        counter += 1     # critical section
    # release (자동)

언어별 Mutex:

언어문법
Javasynchronized(obj) { ... } 또는 ReentrantLock
Pythonwith threading.Lock():
Gosync.MutexLock()/Unlock()
RustMutex<T>.lock() (소유권으로 release 자동)
C++std::lock_guard<std::mutex>

Mutex 흔한 실수:

실수결과
❌ unlock 잊음다른 스레드 영원히 대기
❌ 락 잡고 I/O (DB 호출)처리 시간 길어져 처리량 ↓
❌ 락 안에서 다른 락 잡기Deadlock 위험
❌ 재귀 호출에서 같은 락non-recursive mutex 는 자기 자신과 데드락

ReadWriteLock: 읽기 다중 + 쓰기 단일 → 읽기 많은 워크로드에 유리.
Optimistic Lock: DB 에서 자주 사용. 충돌 시 재시도 (CAS 와 유사).

Deadlock — 영원한 대기

4 가지 필요 조건 (모두 충족 시 데드락):
1. 상호 배제 (Mutual Exclusion) — 자원이 한 번에 1 스레드만
2. 점유와 대기 (Hold and Wait) — 자원 잡고 다른 자원 기다림
3. 비선점 (No Preemption) — 강제 회수 불가
4. 순환 대기 (Circular Wait) — A→B→C→A 순환

고전 예: 두 락 잡는 순서가 스레드마다 다름:

code
Thread 1: lock A → lock B
Thread 2: lock B → lock A

→ 1 이 A 잡고 B 기다림 / 2 가 B 잡고 A 기다림 → 영원

해결 6가지:

방법원리
락 순서 통일모든 스레드가 동일한 순서로 락 획득
타임아웃tryLock(5초) — 실패 시 포기·재시도
데드락 감지DB 처럼 그래프 분석 후 victim 선정
자원 한꺼번에필요한 락 모두 한 번에 (분할 X)
락 회피락 자체 사용 X (Actor·Channel)
타임드 락lock.tryLock(time) 으로 보호

데드락 디버깅:

  • Java: jstack <PID> → "Found one Java-level deadlock" 자동 감지
  • Python: faulthandler + thread dump
  • Go: go run -race (race detector)
  • DB: PostgreSQL 자동 탐지 후 victim 트랜잭션 abort

동기 vs 비동기 · 블로킹 vs 논블로킹

4 가지 조합 (자주 혼동되므로 분리해서 이해):

조합의미예시
동기 + 블로킹결과 나올 때까지 호출자 대기result = db.query(...)
동기 + 논블로킹즉시 반환 (현재 상태). 호출자가 폴링socket.recv() (NONBLOCK)
비동기 + 블로킹작업 시작 후 다른 일. 결과 받을 때 블록Future.get()
비동기 + 논블로킹작업 시작 후 콜백·이벤트로 결과 통지async/await, callback

동기/비동기 = 완료 통지 방식
블로킹/논블로킹 = 호출자 진행 여부

Linux I/O 모델 4가지:

모델시스템 콜비고
블로킹 I/Oread() 기본가장 단순. 1 스레드 = 1 연결
논블로킹 I/OO_NONBLOCK + read()폴링 필요 (CPU 낭비)
I/O 멀티플렉싱select·poll·epoll·kqueue1 스레드가 N 소켓 (Nginx·Redis)
비동기 I/Oio_uring (Linux 5.1+) · POSIX AIO진정한 비동기. 콜백·이벤트로 통지

epoll 동작 (Linux 의 효율적 I/O 멀티플렉싱):
1. epoll_create() — 인스턴스 생성
2. epoll_ctl(ADD) — 감시할 fd 등록
3. epoll_wait() — 이벤트 발생까지 블록
4. fd 처리 → 다시 wait

왜 Node.js·Nginx 가 빠른가 = 단일 스레드 + epoll 이벤트 루프 → 컨텍스트 스위칭 X.

💻 📌 시나리오로 배우는 동시성 디버깅
# ============================================================
# 시나리오 1: "Java 앱이 멈췄다. 데드락인가?"
# ============================================================
# jstack — 모든 스레드 stack trace + *데드락 자동 감지*
jstack 28391 > /tmp/dump.txt
grep -A 50 'Found one Java-level deadlock' /tmp/dump.txt
# 출력 예:
# "Thread-1" waiting to lock <0xabc> (a Account)
#   which is held by "Thread-2"
# "Thread-2" waiting to lock <0xdef> (a Order)
#   which is held by "Thread-1"
# → 두 스레드가 서로 *반대 순서* 락 → 데드락

# 스레드 상태 분석 (BLOCKED·WAITING·RUNNABLE)
grep -E 'java.lang.Thread.State' /tmp/dump.txt | sort | uniq -c
# 예:  150 RUNNABLE  / 5 BLOCKED  / 845 WAITING
# BLOCKED 많으면 = 락 경합 의심

# 더 친절한 도구
jcmd 28391 Thread.print                 # jstack 모던 버전
jconsole &                              # GUI (실시간 스레드·MBean)

# ============================================================
# 시나리오 2: "처리량이 갑자기 떨어짐 — 어디서 시간 쓰지?"
# ============================================================
# 시스템 콜 카운트로 핫스팟 찾기
strace -c -p 28391                # 30초 정도 후 Ctrl+C
# 출력 예:
#   % time  calls  syscall
#   45.2    1820   futex      ← 락 대기 많음! (mutex)
#   22.1    8500   read       ← 디스크 I/O 많음
#   15.3    8500   write
#   ...

# *futex* 가 1위면 → 락 경합 의심
# *read·write* 1위면 → 디스크 I/O 병목

# 특정 시스템 콜만 실시간 추적
strace -e trace=futex,epoll_wait -p 28391

# CPU 핫스팟 (어느 함수가 CPU 먹는지)
perf top -p 28391                 # 대화형
perf record -p 28391 -g sleep 30  # 30초 녹화
perf report                        # 분석

# ============================================================
# 시나리오 3: "Go 앱에 race condition 의심"
# ============================================================
# Go 의 race detector — 컴파일 타임 옵션
go run -race ./cmd/server          # 개발 환경
go test -race ./...                 # CI 에서 항상 권장

# race 발견 시 출력:
# WARNING: DATA RACE
# Read at 0x00c000010080 by goroutine 7:
#   main.counter
# Previous write at 0x00c000010080 by goroutine 6:
#   main.increment
# ⚠️ 두 goroutine 이 동시 접근 = race

# ============================================================
# 시나리오 4: "Python 앱이 hang 됐다"
# ============================================================
# faulthandler 로 현재 모든 스레드 stack 덤프
# 코드 시작 시:
#   import faulthandler
#   faulthandler.enable()
# 그 후 외부에서:
kill -SIGABRT 28391                 # stderr 에 stack 출력

# py-spy — 코드 변경 없이 실시간 프로파일링 (강력)
# pip install py-spy
py-spy dump --pid 28391            # 현재 스택
py-spy top --pid 28391             # 실시간 top (어느 함수가 CPU)
py-spy record -o profile.svg --pid 28391  # flamegraph

# ============================================================
# 시나리오 5: "I/O 가 병목인지 확인"
# ============================================================
# 디스크 I/O 어느 프로세스가 많이?
iotop -o                            # 활성 프로세스만 (-o)

# 디스크별 IOPS·대기 시간
iostat -x 1
# %util > 80%·await > 50ms = 디스크 병목

# 소켓 통계
ss -s                              # 요약 (TCP·UDP·established 수)
ss -tnp                             # TCP 연결 + PID
ss -tn state established sport :443 # 443 으로 들어온 활성 연결

# 열린 fd 수 (자주 만나는 'too many open files')
ls /proc/28391/fd/ | wc -l
cat /proc/28391/limits | grep 'open files'

# ============================================================
# 시나리오 6: "epoll 기반 서버 — 어떤 이벤트 대기 중?"
# ============================================================
# Node.js·nginx·Redis 같은 이벤트 루프 서버
cat /proc/28391/fdinfo/<epoll_fd>   # epoll 인스턴스 정보
# 또는 strace 로 직접:
strace -e trace=epoll_wait,epoll_ctl -p 28391

# 모든 열린 epoll 찾기
ls -la /proc/28391/fd/ | grep eventpoll

🤖 AI 에게 이렇게 요청해보세요

이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다.

  • "이 메서드의 동시성 문제 분석해서 synchronized vs ReentrantLock 추천해줘."
  • "이 코드에 데드락 가능성 있는데 자원 획득 순서로 진단해줘."
  • "이 작업을 CompletableFuture + thenCombine 으로 병렬 처리해줘."

왜 이게 토큰을 줄이나

"락 / 데드락 / volatile / atomic" 어휘를 알면 AI 답변이 바로 코드 로 옵니다. 모르면 "동시성이란..." 부터 다시 설명 받아야 함.

동시성 — 락·데드락·동기/비동기 - 운영체제