정리하기에 앞서 알아두어야할 점은 Synchronous, Asynchronous 와 Blocking, Non-Blocking 은 서로 비교 가능한 개념들이 아니다. 많이들 기술 먼저 습득하다보니(특히 NodeJS), 개념을 먼저 습득하기보다는, 특징을 먼저 배우는 터라 개념이 혼용되어서 쓰이는 경우가 많을 뿐이다.


동기(Sync) 와 비동기(Async)


<Synchronous I/O Model>


Sync와 Async 를 구분하는 기준은 작업 순서이다.

동기식 모델은 모든 작업들이 일련의 순서를 따르며 그 순서에 맞게 동작한다. 즉, A,B,C 순서대로 작업이 시작되었다면 A,B,C 순서로 작업이 끝나야 한다. 설령 여러 작업이 동시에 처리되고 있다고 해도, 작업이 처리되는 모델의 순서가 보장된다면 이는 동기식 처리 모델이라고 할 수 있다. 

많은 자료들이 동기식 처리 모델을 설명할 때, 작업이 실행되는 동안 다음 처리를 기다리는 것이(Wait) Sync 모델이라고 하지만, 이는 잘 알려진 오해이다. 이 Wait Process 때문에 Blocking 과 개념의 혼동이 생기는 경우가 흔하다. 동기 처리 모델에서 알아두어야할 점은 작업의 순서가 보장된다는 점 뿐이다.

동기식 처리 모델은 우리가 만드는 대부분의 프로세스의 로직이며, 특히 Pipeline 을 준수하는 Working Process 에서 매우 훌륭한 모델이다.


<Asynchronous I/O Model>


반면 비동기식 모델은 작업의 순서가 보장되지 않는다. 말그대로 비동기(Asynchronous) 처리 모델로, A,B,C 순서로 작업이 시작되어도 A,B,C 순서로 작업이 끝난다고 보장할 수 없다.

비동기식 처리 모델이 이득을 보는 경우는 각 작업이 분리될 수 있으며, Latency 가 큰 경우이다. 예를들어 각 클라이언트 또는 작업 별로 Latency 가 발생하는 네트워크 처리나 File I/O 등이 훌륭한 적용 예시이다.



블로킹(Blocking) 과 넌블로킹(Non-Blocking)


Blocking 과 Non-Blocking 을 구분하는 기준은 통지 이다.

Blocking 이란 말그대로 작업의 멈춤, 대기(Wait) 을 말한다. 즉, 작업을 시작하고 작업이 끝날때까지 대기하다가 즉석에서 완료 통지를 받는다.

이 때 작업이 멈추는 동안 다른작업이 끼어들수 있는지 없는지는 다른 얘기이다. 이부분이 중요하면서도 헷갈린데, 많은 Blocking 방식의 사례에서 다른 작업의 Interrupt 를 방지하기 때문에, Blocking 은 곧 "순차처리" 로 생각하는 오류가 생긴다. (이렇게 생각해버리면 Blocking 과 Synchronous 모델은 같은 개념이 된다.)

단순히 생각해서 Blocking 은 그저 작업을 수행하는 데 있어서 대기 시간을 갖는다는 의미일 뿐이다.


Non Blocking 이란 작업의 완료를 나중에 통지받는 개념이다. 작업의 시작 이후 완료시까지 대기하지 않고, 완료시킨다. 

즉, 내부 동작에 무관하게 작업에 대한 완료를 처리받는 걸 말한다.

(작업의 예약 부분이 오히려 비동기 설명과 오해를 불러오는 듯 하여 수정했습니다.)

효과적인 작업 상태의 처리를 위해 Non-blocking 에서는 성공, 실패, 일부 성공(partial success) 라는 3가지 패턴이 존재한다.

한 작업에 대해 대기 Queue 와 무관하게 다른 작업을 처리하는 효율적인 알고리즘 처리 시, 각 작업의 완료들의 순서가 보장되지 않는 경우가 많기 때문에 Async 와 헷갈릴 수 있지만, 이들은 앞서 언급했듯이 비교할 수 없는 다른 개념이다.



오해


많은 종류의 소프트웨어에서 동기 처리 방식이 Blocking 이고, 비동기 처리 방식이 Non-Blocking 인 이유는 익숙한 구조이기 때문이다.

일련의 작업들에 대해 순차적으로 하나씩 처리하고 완료하는 방식은 매 작업의 수행마다 Blocking 하는게 작업의 순서를 보장하기 쉬우며,

여러 작업들이 동시에 일어나는 구조에서는 한 작업을 수행하는 동시에 Non-Blocking으로 다른 작업을 받아와서 처리하는 구조가 효율적이다.

그렇기 때문에 같은 개념으로 혼동하기 쉽지만, 동기/비동기와 블로킹/논블로킹은 서로 양립할 수 있는 개념이란걸 기억하자.



정리한 개념을 통해 System I/O 의 모델들에 대해 이해해보자.


- Synchronous Blocking I/O : Application layer 의 계층이 Block을 일으키는 System Call을 호출하고, 이에 따라 User Layer와 Kernel Layer 간의 Context Switching 이 발생한다. 

이 때, Application 은 CPU 를 사용하지 않고 Kernel 의 응답을 기다리게 된다.


- Synchronous Non-blocking I/O : Nonblock Kernel Systemcall 을 사용하기 때문에 더 향상된 것처럼 보이지만, 

User Layer 가 Synchronous 이므로 응답을 기다리는 동안 Kernel 의 System Call을 Polling 하게 된다. 당연히 Context Switching 빈도수가 늘어나기 때문에 더 I/O에 지연이 발생하게 된다.


- Asynchronous Blocking I/O : User Layer의 I/O 가 Non-blocking 이고 Kernel 에서 알림이 블로킹 방식이다. Select System call 이 이 방식의 대표적이며 여러 I/O 를 한번에 수행할 수 있는 모델이다.


- Asynchronous Non-blocking I/O : Kernel I/O 의 개시와 알림 두 차례만 Context Switching 이 발생하고, Kernel 작업이 Non-block 이므로 select() 와 같은 멀티플렉싱 뿐 아니라 다른 프로세싱 자체가 가능하다. 

Kernel level 에서의 응답은 Signal 이나 Thread 기반 Callback 을 통해 user level 로 마치 이벤트처럼 전달된다.


OS 레벨에서 프로세스들의 동작 처리는 비동기 처리 모델 방식으로 진행된다. 각 프로세스 내에서 처리는 작업의 순서가 보장되는 동기 처리 방식이지만, 커널 레벨의 관점에서는 커널의 리소스를 사용하는 모든 작업들의

순서를 지켜줄 수 없으며, 커널 동작의 관점에서 보면 비동기 처리 모델을 사용한다. 

즉, 시스템 프로그래밍 윗 레벨에서의 대부분의 작업 모델은 동기 처리이지만, 커널 레벨에서 프로세스의 운용은 비동기 처리로 볼 수 있다.

물론 각 프로그램들이 동기 / 비동기 중 어떤 방식으로 내부 로직을 처리하는 가는 별도의 문제이다.



추가로 NodeJS 포스팅에서 언급할 예정이지만, NodeJS 는 Async I/O 처리를 위해 libuv 를 사용하는데, 이 엔진은 default thread를 4개 사용하여 Async I/O 를 구현한다. 결국 Async 라 해도 마법은 아니며, 구현의 레벨로 보자면 Scheduling 과 Loop 의 문제이다.


참조 : https://docs.microsoft.com/en-us/windows/desktop/FileIO/synchronous-and-asynchronous-i-o




+ Recent posts