I/O 작업은 Kernel level에서만 수행할 수 있다. 따라서, Process, Thread는 커널에게 I/O를 요청해야 한다.
I/O란 데이터의 입력(input)과 출력(Output)을 함께 일컫는 말이다.
일반적으로 I/O라고 하면 파일 I/O만 생각하는데, 어떠한 기기(디바이스)를 통해 입출력이 이루어지는 작업을 모두 I/O라고 한다.
즉, 네트워크를 통한 데이터 전송, 컨솔 출력 등과 같은 것도 포함된다.
이러한 I/O는 어플리케이션 성능에 가장 많은 영향을 끼친다.
I/O에서 발생하는 시간은 CPU를 사용한 시간과 대기 시간 중에 대기 시간에 속하기 때문에 I/O가 많아진다느 것은 어플리케이션이 연산을 할 때까지 CPU가 아무것도 못하고 대기하는 시간이 길어진다는 의미이고, 이는 어플리케이션의 처리 속도 저하로 이어진다.
따라서 높은 성능을 보장해야하는 어플리케이션에서는 I/O가 큰 장애물이 될 수 있다.
사실 어떤 함수든 일정시간 동안 CPU를 잡아두고 사용하기 때문에 다음 작업 실행을 block한다.
즉, CPU를 사용하는 작업으로 인한 blocking은 개발자가 할 수 있는 부분이 거의 없는데, I/O로 인한 blocking은 CPU를 긴 시간동안 대기시키기 때문에 다른 작업을 할 수 있어도 하지 못해 매우 비효율 적이다.
Blocking/NonBlocking I/O는 호출되는 함수가 바로 리턴하느냐 마느냐가 관심사이다.
Blocking I/O
Non-Blocking I/O
일반적으로 아무런 언급 없이 Blocking I/O와 Non-Blocking I/O를 말한다면 Synchronous(동기) 방식을 말한다.
Blocking I/O는 I/O 작업이 진행되는 동안 User Process가 자신의 작업을 중단한채, I/O가 끝날때까지 대기하는 방식을 말한다.
Blocking I/O 형태의 작업은 다음과 같이 진행된다.
이 경우 말 그대로 block이 되고, 어플리케이션에서 다른 작업을 수행하지 못하고 대기하게 되므로 Resource 낭비가 심하다.
만약 Blocking I/O 방식으로 여러 Client 가 접속하는 서버를 Blocking 방식으로 구현한다면 다음과 같이 진행해야 할 것이다.
이러한 진행으로 인해 많아진 Threads로 context switching 횟수가 증가하므로써, 비효율적인 동작을 하게 된다.
context switch
하나의 프로세스가 CPU를 사용 중인 상태에서 다른 프로세스가 CPU를 사용하도록 하기 위해, 이전의 프로세스의 상태(context)를 보관하고 새로운 프로세스의 상태를 적재하는 작업을 말한다.
한 프로세스의 문맥은 그 프로세스의 프로세스 제어 블록에 기록되어 있다.
이러한 Blocking I/O 방식을 도입할 경우의 문제점을 예를 들자면 카카오톡이 사용자가 메세지를 전송할 때까지 대기하고 있는거라고 생각하면 될 것이다.
카카오톡은 메신저 이외에도 다양한 기능이 많은데 만약 이러한 상황이라면 매우 비효율적일 것이다.
Non-Blocking I/O는 작업이 진행되는 동안 User Process의 작업을 중단하지 않고 I/O 호출에 대해 즉시 리턴하고, User Process가 이어서 다른 일을 수행할 수 있도록 하는 방식을 의미한다.
Non-Blocking I/O 형태의 작업은 다음과 같은 형태로 진행된다.
이러한 과정을 통해 모든 작업 수행이 I/O의 진행시간과는 관계없이 빠르게 동작하기 때문에, User Process는 자신의 작업을 오랜시간 중지하지 않고도 I/O 처리를 수행할 수 있다.
그러나 반복적으로 system call이 발생하기 때문에 이것 또한 Resource 낭비가 된다.
이러한 Non-Blocking I/O는 데이터를 입력할 때만 전송하는 게 아니라 주기적으로 계속 반복하기 때문이다.
그래서 Non-Blocking I/O 문제인 반복적인 system call 호출을 해결하기 위해 I/O 이벤트 통지 모델이 도입되었다.
이벤트란 수신 버퍼나 출력 버퍼에 데이터를 처리하는 동작을 의미한다.
수신 버퍼의 이벤트 -> 입력 버퍼에 데이터가 수신되었다는 것을 알림
출력 버퍼의 이벤트 -> 출력 버퍼가 비었으니 데이터 전송이 가능한 상황을 알림
카톡을 예시로 들면 non-blocking 방식 일시 "너 보낸 메시지 있어?"를 계속 물어봐야 한다. 하지만 이벤트 통지 방식을 사용하면 먼저 입력 버퍼에서 "사용자가 보낸 메시지가 있습니다"라고 알림(이벤트 통지 모델)을 준다면 계속 물어봐야 했던 그 시간을 카카오톡은 이제 효율적으로 사용할 수 있다. 즉, 의존성이 없어진다.
I/O 이벤트 통지 방식에는 동기/비동기(synchronus/asynchronus) 모델로 분류 가능하다.
I/O 작업 상황(결과) 반환 방식에 따라 sync, async 방식으로 분류된다.
Synchronous/Asynchronous는 호출되는 함수의 작업 완료 여부를 누가 신경쓰냐가 관심사다.
Synchronous(동기)
Asynchronous(비동기)
함수가 바로 리턴하지 않고, 호출하는 함수는 작업 완료 여부를 신경쓰는 경우
Sync Blocking I/O는 I/O가 실행되는 동안 어플리케이션이 다른 일을 하지 못하고 있다가, I/O가 끝나고 나서 이어 작업을 처리하는 경우를 말한다.
분리하여 이해해보면 다음과 같다.
system call마다 Thread를 생성하기 때문에 I/O요청이 많은 서비스에서는 작업 한 번의 context switching이 발생하기 때문에 점점 성능이 떨어진다.
또한 block될 동안 커널 응답만 기다리기 때문에 CPU를 사용하지 못한다는 점에서 Resource 사용 관점에서 비효율적이다.
함수가 바로 리턴하고, 호출하는 함수는 작업 완료 여부를 신경쓰는 경우
Sync Non-Blocking I/O는 I/O을 요청하고 즉시 리턴 받아 CPU 제어권을 받는다. 이로써 지속적으로 작업을 실행하면서 I/O 작업이 완료될 때까지 system call을 보내고 I/O가 완료 되면 해당 작업을 처리하는 경우를 말한다.
분리하여 이해해보면 다음과 같다.
커널로부터 제어권을 받기 때문에 Sync Blocking I/O 보다 효율적인 것처럼 느껴질 수 있지만 커널로 부터 결과를 받기까지 계속 상태를 체크하는 busy-wait 상태가 된다. 즉, 작업 order를 맞추기 위해 I/O 작업의 완료를 기다리기 때문에 어떻게 보면 context switching만 빈번하게 일어나는 구조가 될 수 있다.
또한 system call 주기도 적절하게 설정하지 않으면 커널에게 의미없는 요청이 빈번하게 갈 수 있기 때문에 오히려 I/O 작업의 지연을 가져올 수 있다.
함수가 바로 리턴하지 않고, 호출하는 함수는 작업 완료 여부를 신경쓰지 않는 경우
Async Blocking I/O는 I/O 작업을 호출을 할 때 callback을 같이 넘겨주면서, I/O 작업이 종료됐을 때 어플리케이션에 해당하는 callback 함수가 호출되는 방식이지만 실질적으로 I/O 로직이 처리될 때까지 어플리케이션이 block되는 경우를 의미한다.
좀 더 명확히 구분하지면 I/O 작업 자체에 의해 block되는 것이 아니라 system call에 대한 커널의 응답이 block된다고 할 수 있다. 첫 요청에 대해서는 즉각 미완료 상태를 반환하는 non-blocking의 동작을 보여주기 때문이다.
분리하여 이해해보면 다음과 같다.
사실 의도적으로 이 모델을 쓰는 경우는 거의 없다고 할 수 있고, Async Non-Blocking I/O 방식을 사용하는데 그 과정 중에 하나가 Blocking 방식으로 동작하는 경우 Async Blocking I/O 으로 동작할 수 있다.
이러한 Async Blocking I/O의 대표적인 케이스가 Node.js와 MySQL의 조합이라고 한다.
Node.js 쪽에서 callback 통해 Async로 동작해도, 결국 DB 작업 호출 시에는 MySQL에서 제공하는 드라이버를 호출하게 된다.
그런데 이 드라이버가 Blocking 방식이라고 한다.
이러한 경우는 Node.js 뿐아니라 Java의 JDBC도 마찬가지라고 하낟. 다만 Node.js가 싱글 쓰레드 루프 기반이라 멀티 쓰레드 기반인 Java의 Servlet 컨테이너보다 문제가 더 두드러져 보일 뿐이지 Blocking-Async라는 근본 원인은 같다고 한다.
호출되는 함수는 바로 리턴하고, 호출하는 함수는 작업 완료 여부를 신경쓰는 경우
Async Non-Blocking I/O는 어플리케이션은 system call 이후 I/O 처리에 신경 쓰고 있지 않다가 작업이 완료되면 커널로부터 signal, thread 기반 callback 등으로 결과를 마치 event처럼 전달받는 경우이다. 이 경우 응답이 오기 전까지 User Process는 I/O와 독립적인 다른 processing이 가능한 구조이다.
분리하여 이해해보면 다음과 같다.
성능과 자원의 효율적 사용 관점에서 가장 유리한 모델이라고 할 수 있다.
Blocking/NonBlocking I/O는 호출되는 함수가 바로 리턴하느냐 마느냐가 관심사이다.
Synchronous/Asynchronous는 호출되는 함수의 작업 완료 여부를 누가 신경쓰냐가 관심사다.
IBM 아티클에서는 blocking, non-blocking, sync, async 의 차이를 명시하지는 않았지만, 아티클의 내용에 따라서 분류해보자면 다음과 같을 것이다.
- blocking: 작업을 요청하면 일단 요청한 쪽은 일단 block이 되고, 작업이 완료가 된후에 응답을 받을수 있음. 그렇기에 완료가 되기 전에는 요청한 쪽은 block이 되어 다른 작업을 수행하지 못함.
- non-blocking: 작업을 요청하면, 즉시 응답이 돌아옴
- 동기(sync): 작업을 요청한 측에서 작업의 완료 여부를 체크함.
- 비동기(async): 작업을 요청 받은 측에서 작업의 완료 여부를 알려줌
Operating System Concepts 라는 운영체제 교재(흔히 공룡책이라고 부르는 교재)에서도 blocking, non-blocking, async, sync의 차이를 설명하고있다.
- blocking: wait queue 에 들어가고, 시스템 콜이 완료된 후에 응답을 보냄.
- non-blocking: wait queue 에 들어가지 않고, 즉시 리턴함. (응답 또는 에러코드)
- 동기(sync): wait queue 에 머무는게 필수가 아니고, 시스템 콜의 완료를 기다림.
- 비동기(async): 즉시 리턴하고, 시스템콜의 완료을 기다리지않음.
block I/O vs non-block I/O 개념을 설명합니다! 소켓 I/O를 예제로 주로 설명해요! I/O multiplexing(다중 입출력) 설명도 빠질 수 없겠죠? ;) - 쉬운코드
Blocking I/O와 Non-blocking I/O - 널널한 개발자 TV
[10분 테코톡] 🐰 멍토의 Blocking vs Non-Blocking, Sync vs Async - 우아한Tech
https://developer.ibm.com/articles/l-async/
https://www.youtube.com/watch?v=mb-QHxVfmcs
https://etloveguitar.tistory.com/140
https://baek-kim-dev.site/38
https://limdongjin.github.io/concepts/blocking-non-blocking-io.html#ibm-%E1%84%8B%E1%85%A1%E1%84%90%E1%85%B5%E1%84%8F%E1%85%B3%E1%86%AF
https://ko.wikipedia.org/wiki/%EB%AC%B8%EB%A7%A5_%EA%B5%90%ED%99%98
https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous/