본문 바로가기
경제

[임베디드기사 실기 20부작 14편] Mutex, Semaphore, Spinlock, Completion — 동시성 제어와 동기화 완전 이해

by 프레스러쉬 2026. 9. 18.

[임베디드기사 실기 20부작 14편] Mutex, Semaphore, Spinlock, Completion — 동시성 제어와 동기화 완전 이해

임베디드 시스템과 Linux Kernel에서는 여러 Task, Thread, Interrupt Handler가 동시에 같은 자원에 접근합니다. 이때 아무런 보호 없이 공유 데이터를 읽고 쓰면 Race Condition이 발생하고, 심하면 데이터 손상·Deadlock·우선순위 역전·간헐적 오동작으로 이어질 수 있습니다. 이번 편에서는 실기시험의 단골 주제인 Mutex, Semaphore, Spinlock, Completion을 하나의 관점에서 정리해 보겠습니다.

1. 먼저 Critical Section부터 이해하자

Critical Section(임계구역)은 둘 이상의 실행 주체가 동시에 접근하면 데이터의 일관성이 깨질 수 있는 코드 영역입니다.

예를 들어 두 Thread가 동시에 하나의 Counter를 증가시키는 상황을 생각해 보겠습니다.

counter = counter + 1;

이 한 줄은 실제 CPU 수준에서 Read → Modify → Write의 여러 단계로 실행됩니다. 따라서 두 Thread가 동시에 접근하면 증가 연산 하나가 사라지는 Lost Update가 발생할 수 있습니다.

결국 동기화의 목적은 공유 자원에 접근하는 순서를 제어하고 데이터 일관성을 보장하는 것입니다.

2. Mutual Exclusion과 Synchronization은 조금 다르다

Mutual Exclusion(상호배제)은 한 번에 하나의 실행 주체만 공유 자원에 들어오도록 막는 것입니다.

Synchronization(동기화)은 단순히 “한 명씩 들어가라”를 넘어서, 특정 작업이 끝난 뒤 다른 작업이 시작되도록 실행 순서를 맞추는 개념까지 포함합니다.

따라서 Mutex와 Spinlock은 주로 공유 자원의 상호배제에 사용되고, Semaphore는 자원 개수 제어나 동기화에도 활용되며, Completion은 특정 작업의 완료 이벤트를 기다리는 목적에 최적화되어 있습니다.

3. Mutex — 잠들 수 있는 상호배제 Lock

Mutex(Mutual Exclusion)는 한 번에 하나의 Thread만 Critical Section에 들어가도록 보호하는 Lock입니다.

이미 Lock이 잡혀 있으면 다른 Thread는 CPU를 계속 소비하며 기다리지 않고 Sleep 상태로 들어갈 수 있습니다. 따라서 Lock을 오래 기다릴 가능성이 있는 경우 Spinlock보다 효율적입니다.

핵심 특징
① 한 번에 하나의 소유자만 획득
② Lock을 획득하지 못한 Thread는 Sleep 가능
③ Process Context에서 주로 사용
④ Lock을 획득한 실행 주체가 Unlock하는 Ownership 개념이 강함

시험 암기: Mutex는 Sleep 가능한 상호배제 Lock이며, 비교적 긴 Critical Section에 적합하다.

4. Semaphore — 자원의 개수를 관리하는 동기화 도구

Semaphore는 원자적으로 조작되는 정수값을 이용하여 공유 자원에 대한 접근 수를 제한하는 기법입니다.

대표적으로 P 연산(wait/down)V 연산(signal/up)으로 설명합니다.

P 연산은 자원을 획득하는 과정이고, 사용할 자원이 없으면 대기합니다. V 연산은 사용을 마친 자원을 반환하여 기다리는 실행 주체를 깨울 수 있습니다.

Semaphore는 크게 다음 두 형태로 나눌 수 있습니다.

Binary Semaphore — 값이 0 또는 1, 상호배제와 유사한 용도
Counting Semaphore — 여러 개의 동일 자원 개수를 관리

예를 들어 Buffer가 4개라면 Semaphore 초기값을 4로 두고 동시에 최대 4개의 작업만 자원을 사용하도록 제한할 수 있습니다.

5. Mutex와 Semaphore의 차이
항목 Mutex Semaphore
주 목적 상호배제 자원 개수 제어 / 동기화
Lock / Unlock 정수 Counter
소유권 강함 상대적으로 약함
대표 용도 공유 데이터 보호 N개 자원 관리, Task 동기화
6. Spinlock — Sleep하지 않고 계속 확인하는 Lock

Spinlock은 Lock이 풀릴 때까지 Thread가 Sleep하지 않고 반복적으로 상태를 확인하는 Busy-waiting 방식의 Lock입니다.

즉 Lock 획득에 실패한 CPU는 대략 다음과 같이 기다립니다.

while (lock is busy) {
    /* spinning */
}

이 방식은 CPU 시간을 소비하지만, Lock이 매우 짧은 시간 안에 풀린다면 Context Switching을 하는 것보다 오히려 빠를 수 있습니다.

시험 핵심: Spinlock은 Busy-waiting 방식이고, 대기 중 Context Switching이 발생하지 않으며, 짧은 Critical Section에 적합합니다.

7. 왜 Interrupt Context에서는 Mutex를 쓰기 어려운가

Interrupt Handler는 일반 Process처럼 임의로 Sleep할 수 없습니다. 그런데 Mutex는 획득 실패 시 Sleep할 수 있는 Lock입니다.

따라서 일반적인 Hard IRQ Context에서는 Sleep 가능한 Mutex를 사용해서는 안 되고, 매우 짧은 공유 영역을 보호해야 할 때 Spinlock 계열을 사용합니다.

이 차이는 실기시험에서 매우 중요합니다.

Process Context → Mutex 가능
Interrupt Context → Sleep 불가 → Spinlock 고려

8. Spinlock의 장점과 위험

Spinlock은 매우 빠른 동기화 수단이지만 잘못 사용하면 CPU를 낭비합니다.

Critical Section 내부에서 시간이 오래 걸리는 연산, Blocking I/O, Sleep, 복잡한 연산을 수행하면 다른 CPU가 계속 Busy-waiting하기 때문에 성능이 급격히 나빠질 수 있습니다.

따라서 원칙은 간단합니다.

“Spinlock 안에서는 짧고 빠르게 처리한다.”

9. Completion — '자원이 비었나?'가 아니라 '일이 끝났나?'를 기다린다

Completion Variable은 Linux Kernel에서 한 Thread가 다른 Thread 또는 Interrupt Handler의 특정 작업이 완료될 때까지 기다리도록 만드는 동기화 기법입니다.

주요 API는 다음과 같습니다.

init_completion() — Completion 초기화
wait_for_completion() — 작업 완료까지 대기
complete() — 대기자 한 명에게 완료 알림
complete_all() — 모든 대기자에게 완료 알림

Completion은 내부적으로 Wait Queue와 완료 상태를 관리하며, 기다리는 Thread는 CPU를 낭비하며 회전하지 않고 Sleep할 수 있습니다.

10. Completion의 대표적인 사용 예

대표적인 상황은 비동기 I/O입니다.

Main Thread
   ↓
I/O 시작
   ↓
wait_for_completion()
   ↓     (Sleep)
Hardware / Worker Thread
   ↓
작업 완료
   ↓
complete()
   ↓
Main Thread Wake-up

즉 Completion은 “Lock”이라기보다 Event Signaling에 가깝습니다.

11. Completion과 Semaphore의 차이

Semaphore를 초기값 0으로 두고 작업 완료 신호처럼 사용할 수도 있지만, Linux Kernel에서는 특정 작업 완료를 기다리는 목적이라면 Completion이 더 명확합니다.

항목 Completion Semaphore
목적 작업 완료 이벤트 자원/동기화
대기 wait_for_completion() down()/wait
알림 complete() up()/signal
12. 네 가지를 한눈에 비교
기법 Sleep 핵심 목적 대표 환경
Mutex 가능 상호배제 Process Context
Semaphore 가능 자원 개수 / 동기화 Task/Thread
Spinlock 불가 짧은 임계영역 보호 SMP / IRQ
Completion 대기자는 Sleep 작업 완료 알림 Driver / Async I/O
13. Deadlock — Lock을 잘못 사용하면 더 큰 문제가 된다

동기화는 Race Condition을 막기 위한 기술이지만 잘못 사용하면 Deadlock이 발생할 수 있습니다.

예를 들어 Thread A가 Lock 1을 잡고 Lock 2를 기다리고, Thread B가 Lock 2를 잡고 Lock 1을 기다리면 두 Thread가 영원히 진행하지 못합니다.

대표적인 Deadlock 필요조건은 Mutual Exclusion, Hold and Wait, No Preemption, Circular Wait입니다. 이 네 조건 중 하나를 제거하면 Deadlock을 예방할 수 있습니다.

14. Priority Inversion — 실시간 시스템에서 특히 중요하다

Priority Inversion은 높은 우선순위 Task가 낮은 우선순위 Task가 보유한 Lock 때문에 기다리는 상황입니다.

중간 우선순위 Task까지 실행되면 낮은 우선순위 Task가 Lock을 놓는 시점이 더 늦어져 결과적으로 높은 우선순위 Task가 오랫동안 대기할 수 있습니다.

대표적인 해결책은 Priority InheritancePriority Ceiling입니다.

Priority Inheritance에서는 낮은 우선순위 Task가 높은 우선순위 Task의 우선순위를 일시적으로 상속받아 빠르게 Critical Section을 끝내도록 합니다.

15. 실무 장애 분석 관점

현장에서 “가끔 멈춘다”, “부하를 올리면 Hang된다”, “Interrupt 후 Driver가 응답하지 않는다”는 문제는 동기화 결함일 수 있습니다.

다음 순서로 확인하면 좋습니다.

① 공유 변수 또는 Buffer에 Lock이 필요한가
② Lock 획득/해제 쌍이 맞는가
③ Error Path에서 Unlock이 누락되지 않았는가
④ ISR에서 Sleep 가능한 Lock을 사용하지 않았는가
⑤ Spinlock 안에서 오래 걸리는 코드를 실행하지 않는가
⑥ Lock 획득 순서가 일관적인가
⑦ Priority Inversion 가능성이 있는가
⑧ 비동기 작업 완료는 Completion으로 분리하는 것이 적절한가

16. 실기 예상문제와 모범답안

문제 1. Spinlock의 동작방식을 설명하시오.
모범답안: Spinlock은 공유 자원이 해제될 때까지 Sleep하지 않고 반복적으로 Lock 상태를 확인하는 Busy-waiting 방식의 동기화 기법이다. Context Switching이 발생하지 않아 대기시간이 짧은 Critical Section에서 효율적이다.

문제 2. Mutex와 Spinlock의 차이를 설명하시오.
모범답안: Mutex는 Lock 획득에 실패하면 Thread가 Sleep할 수 있어 비교적 긴 대기시간에 적합하다. Spinlock은 Sleep하지 않고 Busy-waiting하므로 짧은 임계구역이나 Interrupt Context와 같은 환경에서 사용된다.

문제 3. Semaphore의 P와 V 연산을 설명하시오.
모범답안: P 연산은 Semaphore 값을 감소시키며 자원을 획득하고, 사용할 자원이 없으면 대기한다. V 연산은 Semaphore 값을 증가시켜 자원을 반환하고 대기 중인 실행 주체를 깨울 수 있다.

문제 4. Linux Kernel의 Completion Variable의 용도를 설명하시오.
모범답안: Completion은 한 Thread가 다른 Thread나 Interrupt Handler의 특정 작업이 완료될 때까지 대기하기 위한 이벤트 기반 동기화 기법이다. wait_for_completion()으로 대기하고 complete()로 완료를 알린다.

17. 시험 직전 30초 암기

Mutex = Sleep 가능한 상호배제 Lock
Semaphore = Counter 기반 자원 관리 / P-V 연산
Spinlock = Busy-waiting / Context Switching 없음 / 짧은 Critical Section
Completion = 작업 완료 이벤트 / wait_for_completion() ↔ complete()
Deadlock = 서로 Lock을 기다리며 진행 불가
Priority Inversion = High Priority가 Low Priority의 Lock 때문에 대기
Priority Inheritance = Low Task가 High Task의 Priority를 임시 상속

18. 마무리 — Lock 이름보다 '기다리는 방식'을 이해하자

동기화 문제는 이름을 외우는 것보다 “기다리는 동안 CPU가 무엇을 하는가”를 이해하면 훨씬 쉬워집니다.

Mutex — 기다리면서 Sleep할 수 있다.
Semaphore — 몇 개의 자원을 사용할 수 있는지 센다.
Spinlock — 기다리면서 계속 돈다.
Completion — 상대 작업이 끝났다는 신호를 기다린다.

이 네 문장을 먼저 기억하고 세부 API와 사용 환경을 붙이면 실기 서술형 답안을 훨씬 안정적으로 작성할 수 있습니다.

다음 편 예고

15편 — Makefile, Cross Compiler, Linking, ELF와 readelf: 소스코드가 실행파일이 되는 과정

태그
#임베디드기사 #임베디드기사실기 #Mutex #Semaphore #Spinlock #Completion #CriticalSection #RaceCondition #Deadlock #PriorityInversion #PriorityInheritance #LinuxKernel #KernelSynchronization #BusyWaiting #ContextSwitching #DeviceDriver #RTOS #동기화 #임베디드시스템 #자격증공부