오늘은 어제 말했던 대로 CPU Scheduling, 그리고 그와 연결되는 동기와 블로킹에 대해 이야기해보겠다.
그 전에 저번 시간에 이야기했던 CPU에서 Thread까지의 내용을 되짚어보자
저번 시간에 이야기했던 내용을 간략하게 줄이면 이정도가 되겠다.
그럼 CPU는 어떤 기준으로 프로세스의 스케쥴을 짤까?
여기서 중요한건 CPU는 직접 프로세스의 스케쥴을 짜지 않는다.
CPU는 단순히 작업 공간이라고 이해하면 된다. 우리가 일터에 갔을 때 일터 자체가 스케쥴을 짜진 않지 않는가? 이와 같이 CPU는 '작업 공간' 그 자체이다.
그렇다면 누가 프로세스의 스케쥴을 짤까?
그건 바로 운영체제라고 부르는 Operating System(OS)이다.
어제도 얼굴을 비추었던 OS는 여러가지 일을 하는데 그 중 하나가 바로 CPU Scheduling이다.
왜 OS는 프로세스의 스케쥴을 짤까?
이유는 아래와 같이 여러가지로 구성된다.
CPU를 가만히 있도록 내비두기 싫어서.
CPU의 능률을 올려주기 위해서.
작업의 대기 시간을 줄이기 위해서.
특정 작업만 돌아가게 하지 않기 위해서.
위와 같은 이유들은 우리가 사장님이라고 생각해보면 바로 느낄 수 있다.
만약 내가 돈을 주고 고용한 직원이 오늘 해야할 일을 모른 채로 가만히 있는다면 어떻겠는가? 또 직원이 잘 안되는 일만 막 붙잡고 계속 가만히 있으면? 또는 컴퓨터로 5분걸리는 작업량을 가진 직원과 3시간걸리는 작업량을 가진 직원중 3시간 걸리는 직원이 사내에 한 대 있는 컴퓨터를 쓰고있는다고 한다면?
생각만해도 비효율적이고, 화가 난다.
이처럼 OS는 효율을 위해서 CPU의 스케쥴을 짠다.
Scheduling은 크게 선점과 비선점 두가지의 범위를 가진다.
이름에서 알 수 있듯
비선점 스케쥴링(non-preemptive scheduling)은 프로세스가 종료되거나 스스로 자리를 양보하기 전에는 다른 프로세스가 그 자리를 뺏았지 못한다.
선점 스케쥴링(Preemptive scheduling)은 비선점과 반대로 OS가 강제로 CPU를 강탈한다.
그로 인해 구현이나 문맥교환에 대한 비용이 낮아진다거나 CPU의 응답률이 올라간다거나 하는 각각의 특징들을 갖는다.
스케쥴링의 종류는 여러가지가 있다.
그중에 우리는 대표적인 5가지에 대해 알아보고자 한다.
FCFS
'First Come First Served'라고도 불리는 FCFS는 들어온 순서대로 먼저 처리하는 순차적인 실행 상태를 가진다. 가장 단순하며, 긴 작업이 발생할 때에는 다른 일이 모두 지연되는 호위효과(Convoy Effect)가 발생할 수 있다.
SJF
'Shortest Job First'라고 불리는 SJF는 이름에서 알 수 있듯 실행시간이 짧은 것부터 먼저 실행한다. 그 대신 실행시간을 매번 파악해야하고, 긴 작업이 계속 밀려나는 기아상태(Starvation)이 발생할 수 있다.
Round Robin
'RR'이라고도 불리는 Round Robin은 프로세스가 일정 시간(Time Quantum)만큼 CPU를 사용할 수 있게 한다. 그로 인해 보편적으로 응답성이 향상된다. 그러나 문맥교환이 잦아지고 FCFS와 비슷하게 동작할 수도 있다.
Priority Schduling
우선순위 스케쥴링은 우선순위가 높은 것부터 실행하는 방식이다. SJF와 동일하게 Starvation이 일어날 수 있다. 그러나 프로세스의 기다림에 따라 우선순위가 상승하는 Aging이라는 절차를 통해 이를 어느정도 해소시키기도 한다.
SRTF
Shortest Remaing Time First라고 불리는 SRTF는 SJF의 선점 버전이라고 봐도 무방하다. 남은 실행 시간이 적은 프로세스를 먼저 실행시키며 더 짧은 프로세스가 오면 CPU를 강탈당한다.
위와 같은 스케쥴링을 통해 한 번에 하나만 실행하는 CPU는 대기 작업을 다시 실행 가능한 상태로 바꿔가며 처리한다. 이때 블로킹/논블로킹, 동기/비동기에 따라 기다림과 실행 방식, 그리고 스케줄링과 CPU 활용이 달라진다.

동기는 작업완료 전까지 대기를 했다가 순차적으로 실행해 나가는 것을 이야기한다.
비동기는 작업 완료를 기다리지 않고 다른 작업을 이어가며, 완료되면 결과를 처리하는 방식이다
우리가 라면을 할 때를 생각해보자.
1. 물을 끓인다.
2. 면과 스프를 넣는다.
3. 라면을 먹는다.
이런 세가지 절차를 가만히 서서 기다리며 한다면 우리는 동기라고 이야기한다.
그러나
우리는 라면을 끓일 때 보통 가만히 있지 않는다. 최소한 나는 그렇다.
1. 물을 끓인다. - '물이 끓는 동안 대파를 썰고 김치를 꺼내온다.'
2. 면과 스프를 넣는다. - '면이 익는 동안 계란을 풀어서 준비해놓고 테이블을 닦는다.'
3. 라면을 먹는다. - '영화를 본다'
이와 같이 무언가 하나의 일만 하지 않고 계속해서 다른 일을 병행하고 있다고 보면 된다. 이것을 우리는 비동기라고 이야기하는 것이다.
동기와 비동기가 결과를 어떻게 처리할 것인지에 대한 내용이라면,
블로킹과 논블로킹은 실행 흐름에 대한 내용이다.

블로킹은 실행 흐름을 끊고 기다리는 것이고,
논블로킹은 기다리는 것 자체를 하지 않고 실행 흐름을 지속하는 것이다.
앞에서 간단하게 동기와 비동기, 블로킹과 논블로킹에 대해 알아보았다.
여기서 필자는 "동기와 블로킹은 같고 비동기와 논블로킹은 같은게 아닌가?
왜 이걸 굳이 둘로 나눈거지?"라는 개인적인 의문이 들었다.
그 이유는 “기다림” 개념에는 실행 흐름 문제와 결과 처리 구조 문제가 섞여 있어서, 이를 분리해야 시스템을 정확하게 설계하고 최적화할 수 있었기 때문이다.
그래서 동기와 비동기는 결과 처리 구조를, 블로킹과 논블로킹은 실행 흐름을 다루는 개념이라고 앞서 설명한 것이다.
그래서 블로킹/논블로킹은 현재 실행 흐름이 멈추는지 여부를 기준으로 하고, 동기/비동기는 결과를 현재 제어 흐름에서 기다려 처리할지 또는 분리된 방식으로 처리할지를 기준으로 한다.
그래서 이러한 동기와 블로킹, 비동기와 논블로킹으로 아래와 같은 조합을 만들어낼 수 있다.

오늘은 CPU 스케줄링부터 시작해서 동기/비동기, 블로킹/논블로킹까지 꽤 긴 여정을 달려왔다.
사실 처음에는 "그냥 기다리면 동기고 안 기다리면 비동기 아니야?"라고 생각했는데, 파고들수록 실행 흐름이냐 결과 처리 구조냐로 개념이 갈린다는 게 꽤 흥미롭지 않았는가?
그렇다면 여기서 한 가지 질문을 던져보겠다.
"여러 스레드가 같은 자원에 동시에 접근하면 어떤 일이 벌어질까?"
다음 시간에는 바로 이 지점에서 시작한다. 동시성 문제가 왜 발생하는지, 그리고 이를 해결하기 위한 동기화 기법에는 어떤 것들이 있는지 알아볼 예정이다. 스포일러를 조금 하자면, 우리가 오늘 배운 블로킹 개념이 거기서도 아주 중요하게 등장한다.
댓글로 위 질문에 대한 생각을 남겨줘도 좋다. 틀려도 괜찮으니 한번 생각해보자.