동기/비동기와 블록킹/논블록킹의 차이점 이해하기

Introduction

프로그래밍의 세계에서는 다양한 상황과 요구에 맞추어 최적의 성능과 효율을 이끌어내는 것이 중요합니다. 이를 위해 개발자들은 '동기(Synchronous)와 비동기(Asynchronous)', '블록킹(Blocking)과 논블록킹(Non-Blocking)'과 같은 기본적이면서도 핵심적인 개념들을 이해하고 활용해야 합니다. 이 개념들은 프로그램의 성능은 물론, 사용자 경험에도 직접적인 영향을 미치며, 효율적인 자원 관리와 멀티태스킹 환경에서의 최적화를 가능하게 합니다. 본 포스팅에서는 이러한 개념들의 정의와 차이점, 그리고 각각의 장단점에 대해 살펴보며, 실제 프로그래밍 환경에서 어떻게 적용될 수 있는지에 대해 알아보겠습니다.

 

1. Blocking vs Non-Blocking

Blocking과 Non-Blocking은 직접 제어할 수 없는 대상을 처리하는 방법에 따라 나눌 수 있습니다. 직접 제어할 수 없는 대상에는 대표적으로 IO, 멀티쓰레드 동기화가 있습니다. Blocking / NonBlocking은 호출되는 함수가 바로 리턴하느냐 마느냐가 관심사다.

Blocking / Non-Blocking : 호출하는 함수가 제어권을 가지고 있는지?

 

1.1 Blocking

Blocking
Blocking

호출된 함수가 자신의 작업을 모두 마칠 때까지 호출한 함수에게 제어권을 넘겨주지 않고 대기하게 만든다면 Blocking 입니다. 예를 들어 호출하는 함수가 IO를 요청했을 때, IO처리가 완료될 때까지 아무 일도 하지 못한 채 기다리는 것을 말합니다.

1.2 Non-Blocking

Non-Blocking
Non-Blocking

반대로, 호출된 함수가 바로 리턴해서 호출한 함수에게 제어권을 넘겨주고, 호출한 함수가 다른 일을 할 수 있는 기회를 줄 수 있으면 Non-Blocking입니다. 예를 들어 호출하는 함수가 IO를 요청한 후 IO처리 완료 여부와 상관없이 바로 자신의 작업을 할 수 있습니다.

 

2. Sync vs Async

Sync와 Async를 다루려면 함께 하는 대상이 누구인지와, 그 대상들의 시간은 어떻게 다루어지는지를 살펴봐야 합니다.

Sync / Async : 호출되는 함수의 완료여부를 신경 쓰는지?

2.1 Synchronous(동기)

동기는 두 가지 이상의 대상(함수, 애플리케이션)이 서로 시간을 맞춰 행동하는 것으로, 1) 작업을 동시에 수행하거나, 2) 동시에 끝나거나, 3) 하나가 끝나는 동시에 다른 것이 시작함을 의미합니다. App은 System call의 완료를 기다리며  kernel에게 완료 여부를 계속 물어보게 됩니다.

동시에 시작 or 종료하는 경우
동시에 시작 or 종료하는 경우

 

리턴 시간과 결과를 전달받는 시간이 일치하는 경우
리턴 시간과 결과를 전달받는 시간이 일치하는 경우

  • A, B 스레드가 동시에 작업을 시작하는 경우(ex: Java의 CyclicBarrier)
  • 메서드의 리턴 시간(A)과 결과를 전달받는 시간(B)이 일치하는 경우
  • A가 끝나는 시간과 B가 시작하는 시간이 같은 경우 (ex: Java의 synchronized와 BlockingQueue)

2.2 Asynchronous(비동기)

비동기는 동기와 반대로 대상이 서로 시간을 맞추지 않는 것을 말합니다. 시작과 종료가 일치하지 않으며, 끝나는 동시에 시작을 하지 않음을 의미한다. App은 System call의 완료를 기다리지 않습니다.

 

3. Blocking/Non-Blocking + Sync/Async

3.1 동기 블록킹 (Synchronous Blocking)

 

Synchronous Blocking
Synchronous Blocking

 

동기 블록킹 방식은 가장 전통적인 처리 모델로, 애플리케이션이 시스템 호출을 통해 I/O 작업을 요청하고, 해당 작업이 완료될 때까지 대기하는 방식입니다. 이 과정에서 사용자 공간과 커널 공간 사이의 문맥 전환(context-switching)이 발생합니다. 애플리케이션은 커널로부터 작업 완료 응답을 받을 때까지 CPU를 사용하지 않고 대기하며, 이는 전반적인 처리 속도를 느리게 만들 수 있습니다.

3.2 동기 논블록킹 (Synchronous Non-Blocking)

Synchronous Non-Blocking
Synchronous Non-Blocking

 

동기 논블록킹 방식은 I/O 작업을 논블록킹으로 요청하지만, 애플리케이션은 작업의 완료 여부를 지속적으로 확인(polling)합니다. 이 방식은 동기 블록킹 모델에 비해 더 자주 시스템 호출과 문맥 전환을 발생시키므로, 오히려 효율성이 떨어질 수 있습니다. I/O 작업이 완료되지 않았다면, 커널은 작업 미완료를 알리는 에러 코드(EAGAIN 또는 EWOULDBLOCK)를 반환합니다.

3.3 비동기 블록킹 (Asynchronous Blocking)

Asynchronous Blocking
Asynchronous Blocking

 

비동기 블록킹 모델에서는 I/O 작업을 블록킹 방식으로 요청하지만, 알림(notification)을 통해 비동기적으로 결과를 받습니다. select()와 같은 시스템 호출을 사용하여 여러 I/O 디스크립터를 동시에 모니터링할 수 있습니다. 하지만 select() 함수 자체가 애플리케이션을 블록 상태로 만들 수 있으며, 고성능 I/O 처리에는 적합하지 않은 문제점을 가집니다.

3.4 비동기 논블록킹 (Asynchronous Non-Blocking I/O)

Asynchronous Non-Blocking I/O
Asynchronous Non-Blocking I/O

 

비동기 논블록킹 방식은 I/O 작업을 요청한 후 즉시 리턴하여, 애플리케이션이 다른 작업을 계속할 수 있게 합니다. I/O 작업은 백그라운드에서 진행되며, 작업 완료 시 신호나 콜백을 통해 결과가 애플리케이션에 전달됩니다. 이 모델은 I/O 작업 중에도 CPU가 유휴 상태에 머무르지 않고 다른 작업을 수행할 수 있어 효율적입니다. 커널은 I/O 작업을 시작하고 완료 알림을 보낼 때만 문맥 전환이 발생합니다.

 

결론: 최적화된 프로그래밍을 위한 전략 선택

동기와 비동기, 블록킹과 논블록킹의 개념을 이해하고 적절히 활용하는 것은 프로그래밍에서 성능 최적화와 자원 관리의 핵심입니다. 각 방식은 고유의 장단점을 가지고 있으며, 상황에 따라 가장 적합한 전략을 선택하는 것이 중요합니다. 동기 방식은 작업의 순서와 완료를 보장하는 반면, 비동기 방식은 멀티태스킹과 빠른 응답 시간을 제공합니다. 마찬가지로, 블록킹 방식은 단순하고 직관적인 코드 작성을 가능하게 하지만, 논블록킹 방식은 자원의 비효율적 사용을 최소화하고, 시스템의 반응성을 높여줍니다. 따라서, 프로그램의 요구 사항과 목표를 명확히 이해하고, 각 방식의 특성을 고려하여 가장 효율적인 솔루션을 선택해야 합니다. 이러한 개념적 이해는 더욱 견고하고 성능이 우수한 소프트웨어 개발의 기반이 됩니다.