비선점형 스케줄링 (Non-preemptive Scheduling)
핵심 인사이트 (3줄 요약)
- 본질: 비선점형 스케줄링 (Non-preemptive Scheduling)은 한 프로세스가 CPU를 할당받으면, 그 프로세스가 I/O를 요청하거나 스스로 실행을 종료 (Exit)할 때까지 운영체제가 중간에 제어권을 강제로 빼앗지 않는 자율적 스케줄링 방식이다.
- 가치: 강제적인 문맥 교환 (Context Switch)이 발생하지 않으므로 오버헤드와 캐시 미스가 최소화되어 시스템 전체의 일괄 처리량 (Throughput) 극대화에 유리하며, 동기화(Race Condition) 문제 설계가 단순해진다.
- 융합: 시분할 대화형 시스템에서는 응답성 부족으로 도태되었으나, 오직 결과 도출 시간이 중요한 슈퍼컴퓨터의 배치 (Batch) 작업이나 단순 임베디드 펌웨어 영역에서는 여전히 효율적인 아키텍처로 사용된다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
- 개념: 프로세스 스케줄링의 초기 패러다임으로, 커널(OS)의 타이머 인터럽트에 의한 강제적인 스위칭(선점) 없이, 오직 Running 상태의 프로세스가 자발적으로 CPU를 놓아주는 시점(Wait 상태로 전이되거나 Terminate 될 때)에만 스케줄러가 개입하여 다음 프로세스를 고르는 방식이다. 협력적 스케줄링 (Cooperative Scheduling)이라고도 불린다.
- 필요성: 컴퓨터의 연산 속도 자체가 느렸던 과거에는 스케줄러가 개입하고 레지스터를 백업하는 문맥 교환 작업 자체가 엄청난 성능 낭비였다. 시스템의 목표가 '여러 사람을 동시에 만족시키는 것'이 아니라 '주어진 거대한 수학 계산을 최대한 빨리 끝내는 것'이라면, 잦은 교체 없이 한 놈에게 끝까지 몰아주는 방식이 가장 효율적이었다.
- 💡 비유: 미용실에서 손님 한 명이 펌과 염색을 모두 마칠 때까지(종료 시점), 대기실에 국회의원이 오든 대통령이 오든 절대로 의자에서 일으켜 세우지 않고 끝까지 서비스를 마치는 **'절대 보장 예약제'**와 같다.
- 등장 배경: 과거 천공 카드를 꽂아 넣던 메인프레임 시절 (일괄 처리 시스템, Batch System)의 기본 스케줄러였다. 작업(Job)을 던져놓고 다음 날 결과를 확인하러 오던 시절에는 응답 시간이 중요하지 않았다. FCFS (First-Come, First-Served)나 단순 SJF (Shortest Job First) 알고리즘이 이 시기에 탄생한 대표적인 비선점 방식이다.
[비선점형 (Non-preemptive) vs 선점형 (Preemptive) 흐름 차이]
(비선점형 흐름 - 자발적 양보 기반)
프로세스 A: [████████ CPU 연산 끝까지 독점 ██████████] (종료/IO 대기)
↓
프로세스 B: (Ready 상태로 무한 대기) ................ [██████ CPU 독점]
(선점형 흐름 - 강제 교체)
프로세스 A: [████] (타이머 강제 쫓겨남) ........ [████]
↓ ↑
프로세스 B: [████] (타이머 쫓겨남) ........
[다이어그램 해설] 비선점형 방식에서는 A 프로세스가 CPU 버스트 구간에 들어가면 어떠한 외부 개입(타이머 인터럽트)도 이를 막지 않는다. 만약 A 프로세스가 잘못 짜인 무한 루프(while(1))를 도는 코드라면, 프로세스 B는 시스템이 재부팅될 때까지 영원히 실행되지 못하는 치명적인 단점(응답성 제로)을 갖는다. 반면 CPU 캐시(L1/L2) 관점에서는 교체 없이 계속 달리므로 캐시 히트율(Hit Rate)이 100%에 수렴하는 완벽한 연산 효율을 보여준다.
- 📢 섹션 요약 비유: 마라톤 릴레이에서 다음 주자에게 바통을 넘기는 타이밍을 감독이 10분마다 강제로 정하는 것(선점)이 아니라, 뛰고 있는 선수가 "나 이제 너무 힘들어서 못 뛰어"라고 스스로 멈출 때까지(비선점) 계속 뛰게 놔두는 전술입니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
스케줄러 개입 시점 (Trigger Points)
비선점형 스케줄러는 프로세스 상태 전이 모델에서 오직 두 가지 경우에만 활성화된다.
- Running → Waiting: 프로세스가 디스크 I/O를 요청하거나
sleep()시스템 콜을 호출하여 자발적으로 CPU를 포기할 때. - Running → Terminated: 프로세스가 모든 코드를 실행하고 정상 또는 비정상 종료 (Exit) 될 때. (※ 선점형 스케줄링은 이 두 가지에 더해 Running → Ready (타이머 만료), Waiting → Ready (우선순위 강탈) 시점에도 개입한다.)
비선점형 시스템의 동기화 (Synchronization) 이점
현대 멀티스레드 프로그래밍이 지옥 같은 이유는 언제 OS가 내 스레드를 멈추고(선점) 다른 스레드를 실행시켜 공유 데이터를 망가뜨릴지 모르기 때문이다. 그러나 단일 코어 기반의 비선점형 스케줄링 환경에서는 이 문제가 완벽하게 사라진다.
┌────────────────────────────────────────────────────────────────────────┐
│ 비선점형 스케줄링에서의 완벽한 임계 구역(Critical Section) 보호 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ [공유 변수: 잔액(Balance) = 100] │
│ │
│ 프로세스 A (50 입금) │ 프로세스 B (30 출금) │
│ -------------------------- │ ----------------------- │
│ 1. Reg1 = Balance (100) │ (A가 끝날 때까지 Ready 큐 대기) │
│ 2. Reg1 = Reg1 + 50 (150) │ (절대 개입 불가) │
│ 3. Balance = Reg1 (150) │ │
│ ==== 🟢 자발적 I/O 양보 또는 작업 종료 (Context Switch) ==== │
│ │ 1. Reg2 = Balance (150) │
│ │ 2. Reg2 = Reg2 - 30(120) │
│ │ 3. Balance = Reg2 (120) │
│ │
│ ✅ 최종 결과: Balance는 정확히 120으로 무결성 100% 보장. │
│ (뮤텍스, 세마포어 등 복잡하고 무거운 락(Lock) 구조가 불필요함) │
└────────────────────────────────────────────────────────────────────────┘
[다이어그램 해설] 프로세스 A가 메모리(잔액)를 읽어서 더하고 다시 쓰는 3단계 과정 동안 시스템은 절대로 A를 쫓아내지 않는다. 따라서 A의 트랜잭션은 자연스럽게 원자성(Atomicity)을 획득한다. 과거 윈도우 3.1이나 Mac OS 9 같은 운영체제(협력적 멀티태스킹)에서는 개발자가 락을 걸 필요가 적었으나, 한 프로그램이 멈추면 OS 전체가 얼어붙는 블루스크린(재부팅 필수) 문제가 일상이었다.
- 📢 섹션 요약 비유: 수술실에서 한 의사가 수술을 시작하면 끝날 때까지 아무도 문을 열고 들어오지 못합니다(비선점). 수술 중간에 의사가 바뀌어 의료 사고(데이터 훼손)가 날 일은 절대 없지만, 바깥에 응급 환자가 와도 수술실 문을 열 수 없다는 치명적인 단점이 있습니다.
Ⅲ. 융합 비교 및 다각도 분석 (Comparison & Synergy)
비선점형 대표 알고리즘 비교 (FCFS vs SJF vs HRN)
| 알고리즘 | 결정 방식 | 특징 및 장점 | 단점 (병목 현상) |
|---|---|---|---|
| FCFS (First-Come, First-Served) | 먼저 도착한 순서대로 실행 | 큐 자료구조 하나로 구현 끝. 극도의 단순함 | 호위 효과 (Convoy Effect): 앞의 무거운 작업 때문에 뒤의 가벼운 작업들이 다 죽어남 |
| SJF (Shortest Job First) | CPU 버스트가 가장 짧은 놈부터 실행 | 수학적으로 평균 대기시간이 가장 짧음 (최적) | 다음 버스트 길이 예측 불가, 긴 작업은 영원히 굶음 (기아 현상) |
| HRN (Highest Response-ratio Next) | (대기시간+서비스시간) / 서비스시간 | 대기 시간이 길어질수록 우선순위를 높여 기아 해결 | 매번 응답 비율을 부동소수점 계산해야 하는 오버헤드 |
호위 효과 (Convoy Effect) 상세 분석
비선점형 알고리즘 중 가장 널리 쓰였던 FCFS의 가장 치명적인 약점은 앞서가는 무거운 트럭(CPU Bound) 한 대 때문에 뒤따르는 수많은 퀵서비스 오토바이(I/O Bound)들이 오도 가도 못하는 호위 효과(Convoy Effect)다.
[호위 효과 (Convoy Effect) 발생 시나리오]
(P1: 100ms짜리 몬스터 연산, P2/P3/P4: 1ms짜리 가벼운 작업)
도착 순서: P1 → P2 → P3 → P4
[FCFS 비선점 실행 흐름]
P1 (100ms) [██████████████████████████████████████████████████]
P2 대기시간: 100ms ───────────────────────────────────────────▶ [█] 1ms
P3 대기시간: 101ms ────────────────────────────────────────────▶ [█] 1ms
P4 대기시간: 102ms ─────────────────────────────────────────────▶ [█] 1ms
>> 평균 대기시간: (0 + 100 + 101 + 102) / 4 = 75.75ms
(만약 P2, P3, P4가 먼저 도착했다면?)
>> 평균 대기시간: (0 + 1 + 2 + 3) / 4 = 1.5ms
이 엄청난 평균 대기시간의 차이(75ms vs 1.5ms)는 시스템의 응답성을 나락으로 떨어뜨린다. I/O 장치를 신속하게 써야 하는 가벼운 프로세스들이 앞선 무거운 연산이 끝날 때까지 완벽하게 블록(Block) 당하기 때문에, CPU와 장치 간의 병렬성이 완전히 붕괴되는 현상이 호위 효과다.
- 📢 섹션 요약 비유: 마트 계산대에서 카트 3개 분량의 장을 본 손님(P1) 뒤에, 껌 하나(P2) 사려는 손님 10명이 서서 30분째 기다리는 발암 유발 상황(호위 효과)과 같습니다. "소량 계산대(선점형)"가 필요한 이유입니다.
Ⅳ. 실무 적용 및 기술사적 판단 (Strategy & Decision)
실무 시나리오
- IoT 임베디드 펌웨어 개발: 자원(RAM 16KB 등)이 극도로 제한된 마이크로컨트롤러(MCU)에서 아두이노(Arduino) 코드를 작성할 때, 보통 하나의 무한
loop()안에서 함수들을 순차적으로 호출하는 Super Loop 방식을 쓴다. 이는 전형적인 비선점형 구조다.- 기술적 판단: 복잡한 선점형 RTOS(FreeRTOS 등)를 올리기엔 타이머 인터럽트와 문맥 스택 저장 오버헤드가 너무 커서 배터리를 낭비한다. 따라서 각 센서 통신 함수가 오래 걸리지 않게 잘게 쪼개서 자발적으로 제어를 반환(Cooperative)하도록 코딩하는 것이 실무적 정석이다.
- 이벤트 루프 기반 언어 (Node.js, Go 코루틴): 단일 스레드 기반의 Node.js V8 이벤트 루프나 Python의
asyncio는 운영체제 관점에서는 선점형 프로세스지만, **유저 레벨 애플리케이션 내부 스케줄링 관점에서는 철저한 비선점형 (협력적 스케줄링)**이다.- 설계 주의점: 개발자가 Node.js의 콜백 함수 내부에서 암호화 해시를 계산하는 무거운 동기적(Synchronous) 루프를 돌리면 이벤트 루프 자체가 블록 되어 다른 모든 사용자의 웹 요청 처리가 멈춘다. 비선점 환경의 본질적 한계이므로 무거운 작업은 워커 스레드로 던져야 한다.
┌───────────────────────────────────────────────────────────────┐
│ 개발 플랫폼에 따른 협력적(비선점) 스케줄링 안티패턴 방어 │
├───────────────────────────────────────────────────────────────┤
│ │
│ Node.js 싱글 스레드 이벤트 루프 (비선점형 구동 방식) │
│ │ │
│ ▼ │
│ 개발자가 while(1) 무거운 루프를 콜백 내부에 작성했는가? │
│ ├─ [예 (동기식 처리)] │
│ │ │ │
│ │ ▼ │
│ │ 전체 시스템 블록 (다른 클라이언트 응답 불가) │
│ │ (비선점형의 가장 치명적인 취약점 발현) │
│ │ │
│ └─ [아니오 (비동기 처리/Yield)] │
│ │ │
│ ▼ │
│ setImmediate() 나 await로 제어권 양보 │
│ │ │
│ ▼ │
│ 다른 이벤트 신속 처리 후 내 코드로 복귀 (정상 동작) │
└───────────────────────────────────────────────────────────────┘
[다이어그램 해설] OS 커널의 비선점형 철학은 현대의 비동기 애플리케이션 아키텍처(Event-Driven)로 그대로 계승되었다. 협력적(비선점) 스케줄링은 락(Lock)을 없애고 쌩쌩 달릴 수 있게 해주지만, "개발자가 예의 바르게(빠르게 제어권을 양보하도록) 코딩했다"는 강한 상호 신뢰를 바탕으로 한다. 악의적이거나 멍청한 코드 한 줄(while true)이 시스템 전체를 마비시키는 한계 역시 똑같이 계승된다.
- 📢 섹션 요약 비유: 규칙 없는 토론장(비선점형)에서는 발언권을 가진 사람이 스스로 마이크를 내려놓을 거라는 '양심'에 모든 걸 의존합니다. 한 사람이라도 눈치 없이 1시간 떠들면(무한 루프) 토론회는 망합니다.
Ⅴ. 기대효과 및 결론 (Future & Standard)
정량/정성 기대효과
| 구분 | 선점형 오버헤드 부과 시 | 비선점형 (협력적) 구동 시 | 기대 효과 |
|---|---|---|---|
| 정량 (문맥 교환) | 초당 수천 번의 스택 저장 및 복원 발생 | 함수 반환 (Return) 수준의 최소 오버헤드 | 문맥 교환에 낭비되는 CPU 사이클 거의 제로화 |
| 정량 (처리량) | 잦은 캐시 플러시(Flush)로 연산 속도 저하 | 뜨거워진 캐시를 끝까지 활용 | 전체 배치 (Batch) 작업 완료 시간 단축 |
| 정성 (코드 복잡도) | 뮤텍스/스핀락 등 데드락 위협 산재 | 단일 실행 흐름으로 동기화 무결성 확보 | 락-프리(Lock-free) 수준의 쉬운 병행 프로그래밍 |
미래 전망
범용 운영체제(Windows, Linux) 수준에서 프로세스 스케줄러로 비선점 방식을 쓰는 시대는 완전히 종말을 고했다. 그러나 그 철학은 경량화되어 유저 공간(User Space)의 **코루틴 (Coroutine - Go, Kotlin, C++20)**과 파이버 (Fiber) 구조로 화려하게 부활했다. OS가 무식하게 뺏는 선점형 컨텍스트 스위칭의 비용을 감당할 수 없게 된 초고도 동시성 서버 환경에서, 개발자가 직접 yield와 await 키워드로 우아하게 양보하는 비선점형(협력적) 스케줄링이 차세대 고성능 백엔드의 표준 패러다임이 되고 있다.
- 📢 섹션 요약 비유: 공권력(OS 커널)이 강제로 몽둥이로 치면서 뺏던 선점의 시대에서, 이제는 수준 높은 시민(코드)들이 알아서 "양보(await)" 표지판을 보고 차례를 양보하는 선진적인 협력의 시대(코루틴)로 회귀하고 있습니다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| 호위 효과 (Convoy Effect) | 비선점형 방식(특히 FCFS)의 치명적 결함으로, 하나의 무거운 연산 뒤에 가벼운 I/O 바운드들이 줄지어 적체되는 병목 현상이다. |
| 문맥 교환 (Context Switch) | 비선점형은 이를 프로세스가 원할 때만 수행하므로 캐시를 100% 보존하지만, 응답성이 파괴된다. |
| 코루틴 (Coroutine) | 현대 프로그래밍 언어에서 비선점형(협력적) 스케줄링 철학을 유저 레벨의 경량 스레드로 부활시킨 최신 동시성 모델이다. |
| 일괄 처리 시스템 (Batch System) | 응답 지연은 무시하고 시스템 전체의 연산 처리량(Throughput)이 중요했던 과거 메인프레임 시대로, 비선점 스케줄링이 활약한 주 무대다. |
| 경쟁 조건 (Race Condition) | 비선점형 환경에서는 내가 양보하기 전까진 남이 낄 수 없으므로, 공유 데이터를 다룰 때 이 위험에서 구조적으로 해방된다. |
👶 어린이를 위한 3줄 비유 설명
- 미끄럼틀을 탈 때 "딱 3번만 타고 무조건 다음 사람에게 비켜!"라고 강제로 뺏는 게 '선점형'이에요.
- 반면에 비선점형은 한 친구가 미끄럼틀을 잡았으면 그 친구가 "나 이제 그만 탈래 재미없어"하고 스스로 내려올 때까지 절대 뺏지 않고 기다려주는 방식이랍니다.
- 중간에 방해받지 않으니까 타고 있는 친구는 흐름 안 끊기고 최고로 재밌게 놀 수 있지만, 뒤에서 기다리는 친구는 언제 탈 수 있을지 몰라 답답해 미칠 수 있어요!