Skip to content
난이도 면접 빈출문제은행 3

동기화 ​

여러 스레드(thread)가 같은 데이터를 동시에 고치면, 실행 순서에 따라 결과가 달라지는 경쟁 상태(race condition)가 생긴다. 동기화(synchronization)는 이런 공유 데이터 접근의 순서를 정해 결과를 항상 같게 만드는 일이다. 이 문서는 가장 기본 도구인 뮤텍스(mutex)와 세마포어(semaphore)를 다룬다. 둘 다 "한 번에 누가 들어갈 수 있는가"를 정하는 도구이고, 차이는 잠근 쪽만 풀 수 있느냐와 몇 개까지 들여보내느냐에 있다. 스핀락(spinlock)·모니터(monitor) 같은 다른 도구는 동기화 2에서 이어진다.

경쟁 상태: count++는 한 번에 끝나지 않는다 ​

두 스레드가 전역 변수 count를 각각 count++로 한 번씩 올리면 2가 늘어야 한다. 그런데 가끔 1만 는다. count++는 한 줄이지만 CPU 안에서는 메모리에서 읽기, 더하기, 메모리에 쓰기의 세 단계로 나뉘기 때문이다. 아래는 이를 세 개의 기계어(machine code) 명령으로 옮긴 모습이다. 컴파일러(compiler)가 메모리 값을 바로 더하는 명령 하나로 만들더라도, 중간에 다른 스레드가 끼어들 수 없이 한 번에 끝나는 원자적 연산(atomic operation)으로 실행하라는 특별한 명령이 아니면 CPU 안에서는 같은 세 단계로 나뉜다.

text
load  r1, count   ; 메모리의 count를 레지스터로 읽기
add   r1, 1       ; 레지스터 값에 1 더하기
store r1, count   ; 결과를 메모리에 쓰기

스레드 A가 load까지 한 뒤 문맥 교환(context switch)으로 멈추면 문제가 생긴다. 처음 count가 0이라고 하고 시간 순서대로 따라가 보자.

순서스레드 A스레드 B메모리의 count
1load → 레지스터(register)에 00
2(문맥 교환으로 멈춤)load → 00
3add → 1, store1
4add → 1, store1

두 번 올렸는데 결과는 1이다. A는 B가 바꾸기 전의 옛 값을 레지스터에 들고 있다가 그 값에 1을 더해 덮어썼고, 그래서 B의 증가분이 사라졌다. 멀티코어(multi-core)에서는 두 스레드가 정말로 동시에 실행되므로 문맥 교환이 없어도 같은 일이 생긴다.

임계 구역과 해결 조건 ​

임계 구역(critical section)은 공유 자원에 접근하는 코드 구간이다. 위 예에서는 count++ 세 명령이 임계 구역이다. 임계 구역 문제를 올바르게 풀려면 세 조건을 만족해야 한다.

조건뜻
상호 배제(mutual exclusion)한 스레드가 임계 구역에 있으면 다른 스레드는 들어갈 수 없다
진행(progress)임계 구역이 비어 있으면, 들어가려는 스레드 중 하나는 반드시 들어갈 수 있다
한정 대기(bounded waiting)들어가려고 기다리는 스레드가 무한정 밀리지 않는다

상호 배제는 정확성을 위한 조건이다. 진행은 아무도 없는데 서로 양보만 하다 멈추는 일을 막는다. 한정 대기는 특정 스레드만 계속 순서를 빼앗겨 영원히 못 들어가는 기아(starvation)를 막는다.

뮤텍스 ​

뮤텍스는 임계 구역 앞뒤에 거는 잠금이다. 들어갈 때 lock()으로 잠그고 나올 때 unlock()으로 푼다. 이미 잠겨 있으면 뒤에 온 스레드는 잠든 상태로 기다리고, 풀리면 기다리던 스레드 중 하나가 깨어나 잠근다.

뮤텍스의 핵심은 소유권이다. 잠근 스레드가 그 잠금의 주인이고, 주인만 풀 수 있다. 다른 스레드가 풀려고 하면 오류가 나거나, 규격이 결과를 정해 두지 않은 정의되지 않은 동작(undefined behavior)이 된다. 즉 무슨 일이 일어날지 보장할 수 없다. 예를 들어 Java의 ReentrantLock은 주인이 아닌 스레드가 unlock()을 부르면 IllegalMonitorStateException을 던진다.

세마포어 ​

세마포어는 정수 값 하나와 두 연산으로 이뤄진 동기화 도구다. 네덜란드의 컴퓨터 과학자 에츠허르 다익스트라(Edsger Dijkstra)가 제안해 연산 이름을 P와 V로 쓰기도 한다.

  • P 연산(wait): 값이 0보다 크면 1 줄이고 들어간다. 0이면 값이 생길 때까지 기다린다.
  • V 연산(signal): 값을 1 늘리고, 기다리는 스레드가 있으면 하나를 깨운다.

초깃값이 N이면 최대 N개 스레드가 동시에 들어갈 수 있다. 이렇게 여러 개를 허용하는 것을 카운팅 세마포어(counting semaphore)라고 하며, 데이터베이스 연결을 미리 5개 만들어 두고 돌려 쓰는 커넥션 풀(connection pool)처럼 개수가 정해진 자원을 나눠 줄 때 쓴다. 값을 0과 1로만 쓰면 이진 세마포어(binary semaphore)다.

기준뮤텍스세마포어
정체잠금정수 카운터
동시에 들어가는 수1초깃값 N까지
소유권있다. 잠근 스레드만 푼다없다. 어느 스레드든 signal할 수 있다
주된 용도임계 구역 보호자원 개수 제한, 실행 순서 신호

이진 세마포어는 뮤텍스와 비슷해 보이지만 같지 않다. 소유권이 없어서 A가 wait하고 B가 signal해도 된다. 이 성질 덕분에 "B가 일을 끝내면 A를 진행시킨다" 같은 순서 신호에 쓸 수 있다. 반대로 잠그지 않은 스레드가 실수로 풀어도 막아 주지 않는다.

이어지는 내용은 동기화 2에서 다룬다. 잠들지 않고 기다리는 스핀락(spinlock), 조건을 기다리는 모니터와 조건 변수(condition variable), 잠금 없는 CAS(Compare-And-Swap, 비교 후 교환), 그리고 한쪽이 만든 데이터를 버퍼에 넣고 다른 쪽이 꺼내 쓰는 생산자-소비자 문제(producer-consumer problem)를 세마포어로 푸는 방법을 본다.

면접 포인트 ​

  • "뮤텍스와 세마포어의 차이는?" → 뮤텍스는 소유권 있는 잠금(1개), 세마포어는 소유권 없는 카운터(N개). 이진 세마포어도 소유권이 없어 뮤텍스와 같지 않다.
  • "count++가 왜 위험한가요?" → 읽기·더하기·쓰기 세 명령으로 나뉘어 중간에 끼어들 수 있다.
  • "임계 구역 문제의 해결 조건은?" → 상호 배제·진행·한정 대기. 한정 대기가 기아를 막는다.
  • DB의 행 잠금도 같은 문제를 푼다 → InnoDB 잠금.

관련 문서