동시성은 결코 쉽지 않다. 먼저 현실을 직시하자.
동시성은 항상 성능을 높여준다 — 대기 시간이 아주 길어 여러 스레드가 프로세서를 공유하거나, 여러 프로세서가 처리할 독립적인 계산이 충분히 많은 경우에만 성능이 향상된다.
동시성을 구현해도 설계는 변하지 않는다 — '무엇'과 '언제'를 분리하면 설계가 크게 변한다.
동시성은 다소 부하를 요구한다 — 성능 오버헤드와 추가 코드가 수반된다.
동시성 버그는 재현이 어렵다 — 타이밍에 의존하므로 같은 조건에서도 다르게 동작할 수 있다.
근본적인 설계 전략 재고 필요 — 동시성은 단순히 코드를 추가하는 문제가 아니다.
동시성 문제를 예방하기 위한 설계 원칙들.
동시성 관련 코드는 다른 코드와 분리하라. 스레드 관리 코드와 비즈니스 로직을 섞지 마라.
스레드 간 공유 자원에 동시 접근하면 예상치 못한 결과가 생긴다. synchronized/lock을 사용하라.
private readonly object _lock = new object(); private int _counter = 0; public void Increment() { lock (_lock) { _counter++; } }
임계 영역을 만들기 어렵다면 차라리 객체를 복사해서 읽기 전용으로 사용하는 것도 좋다. 복사 비용이 동기화 비용보다 저렴할 수 있다.
스레드는 가능한 독립적으로 구현하라. 각자의 자원만 사용하고, 자료를 독자적으로 처리 가능하게 분리하라. 공유 자원 없음 = 경쟁 조건 없음.
생산자는 큐에 넣고 소비자는 큐에서 꺼낸다. 두 역할의 속도 차이를 버퍼로 완충한다.
Semaphore: 빈 슬롯 수와 아이템 수를 세는 세마포어 + 뮤텍스로 구현
Blocking Queue: put()/take()가 내부적으로 대기/신호를 처리해주는 큐 구현체 사용
읽기는 동시에 가능하지만 쓰기는 단독으로만 가능하다.
Reader 우선 정책: 처리량↑ 이지만 Writer가 기아(Starvation) 상태에 빠지고 공유 자원이 정체된 정보로 가득 찬다.
Writer 우선 정책: 데이터는 최신으로 유지되지만 처리량이 줄어든다.
RW Lock: 읽기 시엔 읽기 접근은 통과, 쓰기 접근은 차단. 쓰기 시엔 모든 접근 차단.
FIFO 정책: 요청 순서대로 처리해 Starvation 방지
자원 공유 패턴에서 발생하는 교착·기아·비효율 문제의 원형.
전원이 왼쪽 포크만 잡고 서로 기다리면 영원히 멈춤. 아무도 오른쪽 포크를 가져갈 수 없다.
서로 양보만 반복 — 포크를 집었다 놨다를 반복하지만 실제로 아무도 식사하지 못한다.
특정 철학자는 계속 굶주린다 — 운이 나쁘면 영원히 포크를 못 잡을 수 있다.
과도하게 보수적인 설계 — 한 번에 한 명만 식사하게 하면 안전하지만 병렬성이 낮아진다.
Resource Hierarchy (Ordered Locks): 포크에 번호를 매겨 항상 낮은 번호부터 잡는다. 순환 대기가 원천 차단된다.
타임아웃 + 재시도: 일정 시간 동안 포크를 못 잡으면 포기하고 재시도. 데드락 방지.
동시성을 안전하게 다루기 위한 핵심 전략
성능 향상이 보장되지 않고, 버그는 재현이 어렵다.
동시성 코드와 비즈니스 로직을 분리. SRP를 적용하라.
공유 자원과 임계 영역을 최소화하라. 읽기 전용 객체 복사도 고려하라.
다양한 설정에서 여러 번 테스트하라. 타이밍 버그는 한두 번 실행으로 잡히지 않는다.