Deadlock과 큐 vs 스레드 완전 정리
Deadlock, Self-Deadlock, Blocking의 차이와 큐와 스레드의 관계를 Q&A 형식으로 정리
1️⃣ Deadlock의 종류
데드락(Deadlock)이란?
스레드들이 서로의 자원을 쥔 채 안 놓고 상대를 기다려 영영 진행 못 하는 순환 대기 상태입니다.
main.sync를 메인 스레드에서 호출하면?
Self-Deadlock이 발생합니다. 메인 큐는 메인 스레드에서만 실행되는데, 메인 스레드가 sync로 멈춰 기다리므로 자기 자신을 기다리는 상황이 됩니다.
- 고전 데드락: 스레드 2개 이상이 서로의 자원을 쥔 채 순환 대기
- Self-deadlock: 스레드 1개가 자기 자신을 기다림
- 공통점: 방아쇠는 항상
sync (기다림). async로 바꾸면 안 기다려서 데드락 안 남.
순환 대기 예시는 두 serial 큐가 서로 cross-sync 하는 것?
맞습니다. 단, 두 큐가 동시에 각자 점유 중일 때만 터지는 타이밍 의존 데드락입니다.
2️⃣ 큐 vs 스레드
큐는 스레드에게 태스크를 넘기는 것뿐?
맞습니다.
- 큐 = 태스크 대기열 + 실행 규칙
- 스레드 = 실제 일꾼
- Serial 큐 ≠ 전용 스레드 1개 (메인 큐만 메인 스레드 고정)
serial 큐가 실행 중이면 다른 태스크를 받지도 못하나?
아니요. 접수(enqueue)는 항상 가능하고, 직렬화되는 건 실행(스레드에 넘겨 돌리기)뿐입니다.
3️⃣ async vs sync의 진짜 의미
"어떻게 제출됐나(async)"와 "실행 중에 뭘 하나(sync로 멈춤)"는 별개다?
정확합니다. 제출이 async였어도, 일단 실행에 들어가면 안의 sync 때문에 멈춥니다.
"sync=블로킹, async=논블로킹" 맞나?
절반만 맞습니다.
- sync는 호출자 스레드를 블로킹함 ✅
- async라도 그 안에서 블로킹 호출을 하면 워커 스레드가 막힙니다 ❌
- blocking 여부는 큐가 아니라 "어떤 함수를 부르냐"가 결정합니다.
async 블록 내부에서 sync를 호출하면 그 (워커) 스레드는 블록됩니다. (thread explosion / cross-queue 데드락의 메커니즘)
4️⃣ 블로킹 I/O vs 논블로킹 I/O
논블로킹 I/O란?
기다리는 동안 어떤 스레드도 park 안 하고, OS 이벤트 알림으로 완료를 통보받는 방식입니다.
예: URLSession, await
블로킹 호출의 예시는?
- Data(contentsOf:) - 동기 파일/네트워크 읽기
- Thread.sleep()
- DispatchQueue.sync
- NSLock.lock()
- Semaphore.wait()
→ 모두 스레드를 잡아둠 (park)
-
await 사용 시 자동으로 논블로킹- Cooperative Thread Pool (CPU 코어 수만큼만 스레드 생성)
- Thread explosion 방지
5️⃣ Blocking vs Deadlock
Thread Explosion은 블로킹이지 데드락이 아니다?
정확합니다!
- Blocking: 느리지만 시간이 지나면 결국 진행됨
- Deadlock: 영원히 진행 불가 (순환 대기)
- "시간 지나면 풀리는가?" → Yes: Blocking, No: Deadlock
- "자원을 잡고 있는가?" → lock()했지만 unlock() 안 한 상태
6️⃣ Thread Explosion (스레드 폭발)
문제 1: Concurrent Queue + 블로킹 호출
블로킹:
스레드: [작업 시작]──────대기──────[응답 받음]
↑ ↑
스레드 점유 중 (아무것도 못함)
비동기:
스레드: [작업 시작][다른 작업...]
↓
콜백: [응답 받음 → 처리]
문제 2: Private Concurrent Queue 남발
해결: Serial Queue는 target을 Global로
target 없이 Serial Queue 여러 개:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Serial A │ │ Serial B │ │ Serial C │
│ └─ 스레드 1 │ │ └─ 스레드 2 │ │ └─ 스레드 3 │
└──────────────┘ └──────────────┘ └──────────────┘
→ 각자 스레드 관리 → 큐 많아지면 스레드 폭발
target: global() 설정:
┌──────────────┐
│ Serial A ────┼───┐
├──────────────┤ │ ┌─────────────────────┐
│ Serial B ────┼───┼──→│ Global Queue │
├──────────────┤ │ │ └─ 스레드 풀 (공유) │
│ Serial C ────┼───┘ └─────────────────────┘
└──────────────┘
→ 직렬성은 유지, 스레드는 공유
- Serial Queue의 직렬 실행 보장은 유지됨 (A 작업끼리는 순서대로)
- 실제 스레드는 Global Queue 풀에서 할당 (스레드 수 절약)
블로킹 네트워크 API 예시
📌 핵심 정리
1. Deadlock의 종류
- 고전 Deadlock: 2개 이상 스레드가 서로의 자원 순환 대기
- Self-Deadlock: 1개 스레드가 자기 자신 기다림 (예: main.sync on main)
- 타이밍 의존 Deadlock: 두 큐가 동시 점유 중일 때만 발생
- 공통점: 방아쇠는 항상
sync
2. 큐 vs 스레드
- 큐 = 태스크 대기열 + 실행 규칙
- 스레드 = 실제 일꾼
- Serial 큐 ≠ 전용 스레드 (메인 큐만 예외)
- enqueue는 항상 가능, 직렬화는 실행만
3. async vs sync
- "제출 방식(async/sync)"과 "실행 중 동작"은 별개
- async 블록 안에서 sync 호출하면 워커 스레드 블로킹됨
- async ≠ 논블로킹 (어떤 함수를 부르냐가 결정)
4. 블로킹 I/O vs 논블로킹 I/O
- 블로킹: 스레드 park (Data(contentsOf:), Thread.sleep, lock)
- 논블로킹: 스레드 반환 후 이벤트 통보 (URLSession, await)
- Swift Concurrency는 await로 자동 논블로킹
5. Blocking vs Deadlock
- Blocking: 느리지만 결국 진행됨
- Deadlock: 순환 대기, 영원히 진행 안 됨
- Thread explosion = Blocking (Deadlock 아님)