I'm FanJae.

Unity 게임 개발 캠프 개인 프로젝트 3일차. 동기와 비동기, Blocking과 Non-Blocking (개인 공부 내용) 본문

Projects/MyToyMapleServer

Unity 게임 개발 캠프 개인 프로젝트 3일차. 동기와 비동기, Blocking과 Non-Blocking (개인 공부 내용)

FanJae 2026. 9. 16. 23:01

1. 시작에 앞서

- 네트워크 서버를 구현하다 보면 동기(Synchronous), 비동기(Asynchronous), Blocking, Non-Blocking이라는 용어를 자주 접하게 된다.

- 처음에는 단순히, 동기는 작업을 기다리는 방식이며, 비동기는 작업을 기다리지 않는 방식 정도로 생각하기 쉽다.

- 하지만 실제로 네트워크 프로그래밍을 시작하면 Blocking Socket, Non-Blocking Socket, Asynchronous I/O, Overlapped I/O, IOCP 등의 개념이 등장하면서 이 구분만으로는 설명하기 어려워진다.

- 특히, 동기/비동기와 Blocking/Non-Blocking은 서로 완전히 동일한 기준으로 구분하는 개념이 아니다.

- 따라서 IOCP를 본격적으로 정리하기 전에 먼저 이 네 가지 개념을 구분해두려고 한다.


2. 동기(Synchronous)

2.1 동기란?

- 동기 방식은 작업을 요청한 뒤 해당 작업의 완료 흐름과 호출자의 실행 흐름이 연결되어 있는 방식이라고 볼 수 있다.

- 예를 들어 다음과 같은 작업이 있다고 가정한다.

ReadData();
ProcessData();

- ReadData()가 동기적으로 동작한다면 ReadData()의 작업이 끝난 뒤 다음 코드인 ProcessData()가 수행된다.

Thread

ReadData()
    │
    │ 작업 수행
    │
    ▼
 작업 완료
    │
    ▼
ProcessData()

- 즉 호출자는 해당 작업의 완료를 기준으로 다음 작업을 진행한다.

- Windows의 동기 I/O에서도 스레드가 I/O 작업을 시작하면 I/O 요청이 완료될 때까지 해당 스레드가 대기 상태에 들어갈 수 있다.


2.2 동기의 장점

- 동기 방식은 프로그램의 실행 흐름을 이해하기 쉽다는 장점이 있다.

LoadData();
ProcessData();
SaveData();

- 코드의 작성 순서와 실제 처리 순서가 비교적 명확하게 대응한다.

- 따라서 구현이 단순하고, 디버깅이 비교적 쉽고, 작업 순서를 보장해야 하는 경우 사용하기 편하다.


2.3 동기의 문제점

- 문제는 작업 중간에 시간이 오래 걸리는 I/O가 포함될 경우이다.

- 예를 들어 네트워크에서 데이터를 받아야 하는 상황을 생각해볼 수 있다.

recv(socket, buffer, size, 0);

- 데이터가 아직 도착하지 않았고 해당 호출이 작업 완료를 기다리게 된다면, 그 시간 동안 해당 스레드는 다른 작업을 처리하지 못할 수 있다.

Thread

recv()
  │
  │
  │ 데이터가 도착할 때까지 대기
  │
  │
  ▼
return

- 클라이언트가 한두 명이라면 큰 문제가 아닐 수 있지만, 처리해야 하는 연결이 증가할수록 이러한 대기 시간이 서버 자원 활용에 영향을 줄 수 있다.


3. 비동기(Asynchronous)

3.1 비동기란?

- 비동기 방식에서는 작업을 요청한 뒤 작업이 끝나는 시점까지 호출 흐름을 계속 묶어두지 않고, 작업 완료를 별도의 방식으로 처리한다.

- 대략적인 흐름은 다음과 같다.

작업 요청
    │
    ▼
I/O 진행 ───────────────┐
                        │
호출자는 다른 작업 수행 │
                        │
                        ▼
                    I/O 완료
                        │
                        ▼
                    완료 통지

- Windows의 비동기 I/O에서는 호출 스레드가 커널에 I/O 요청을 전달한 뒤 다른 작업을 수행할 수 있으며, 이후 I/O가 완료되었음을 전달받아 결과를 처리할 수 있다.

- 따라서 중요한 점은, 비동기는 여러 작업이 동시에 실행된다의 개념이 아니다.

- 중요한 것은 작업 요청과 작업 완료 처리가 분리되어 있다는 점이다.


3.2 비동기가 필요한 이유

- 네트워크 I/O는 CPU 연산과 다르게 작업이 즉시 완료된다는 보장이 없다.

- 예를 들어 클라이언트에게 데이터를 요청했다고 가정해보자.

서버
 │
 │ 요청
 ▼
클라이언트
 │
 │ 네트워크 이동
 │
 ▼
서버 데이터 수신

- 네트워크 상태에 따라 데이터가 도착하기까지 시간이 걸릴 수 있다.

- 아래와 같은 동작으로 움직이게 된다면 하나의 스레드가 여러 작업을 보다 효율적으로 관리할 수 있다.

I/O 요청
   ↓
다른 작업 처리
   ↓
I/O 완료
   ↓
완료된 작업 처리

- 다만 빠르게 완료되는 작업에서는 비동기 처리 자체의 오버헤드 때문에 항상 비동기가 유리한 것은 아니다.


4. Blocking과 Non-Blocking

- 동기와 비동기를 이해할 때 같이 등장하는 개념이 Blocking과 Non-Blocking이다.

- 여기서 중요한 것은 두 개념을 같은 의미로 사용하면 안 된다는 점이다.

- 동기/비동기가 작업의 완료 처리 방식과 관련된 개념이라면, Blocking/Non-Blocking은 함수를 호출한 스레드의 제어권이 어떻게 되는지에 초점을 맞춘다.


5. Blocking

- Blocking은 특정 함수를 호출했을 때 작업이 진행되는 동안 호출한 스레드의 실행이 해당 호출에 묶이는 것을 의미한다.

- 네트워크에서 가장 대표적인 예가 recv()이다.

recv(socket, buffer, size, 0);

- Blocking Socket에서 아직 수신된 데이터가 없다면 recv()는 데이터가 들어올 때까지 반환하지 않을 수 있다.

Worker Thread

recv()
 │
 │
 │ 데이터 대기
 │
 │
 ▼
데이터 도착
 │
 ▼
return

- 그동안 해당 Worker Thread는 다른 작업을 수행할 수 없다.

- recv, recvfrom, send, sendto 등의 작업은 Blocking Socket에서 즉시 완료할 수 없는 경우 호출을 차단할 수 있다고 설명한다.


6. Non-Blocking

- Non-Blocking에서는 작업을 즉시 완료할 수 없다면 호출을 계속 붙잡고 있지 않고 결과를 즉시 반환한다.

- 예를 들어 Non-Blocking Socket에서 recv()를 호출했다고 가정한다.

int result = recv(socket, buffer, size, 0);

if (result == SOCKET_ERROR)
{
    int error = WSAGetLastError();

    if (error == WSAEWOULDBLOCK)
    {
        // 현재 읽을 데이터가 없음
    }
}

- 수신할 데이터가 없다면 데이터를 기다리는 대신 WSAEWOULDBLOCK을 반환한다.

recv()
 │
 ▼
데이터 존재?
 │
 ├─ YES → 데이터 반환
 │
 └─ NO  → WSAEWOULDBLOCK 반환

- 따라서 호출한 스레드는 다른 작업을 진행할 수 있다.

- Non-Blocking Socket의 I/O는 즉시 완료되거나, 즉시 완료할 수 없는 경우 WSAEWOULDBLOCK을 반환한다고 설명한다.


7. 그렇다면 Non-Blocking은 비동기인가?

- 여기서 가장 헷갈렸던 부분이다.

Blocking = 동기
Non-Blocking = 비동기

- 하지만 두 개념의 기준은 다르다.

- 예를 들어 다음과 같은 코드를 생각해볼 수 있다.

while (true)
{
    int result = recv(socket, buffer, size, 0);

    if (result == SOCKET_ERROR)
    {
        if (WSAGetLastError() == WSAEWOULDBLOCK)
            continue;
    }

    break;
}

- recv() 자체는 데이터를 기다리지 않고 바로 반환한다.

- 따라서 Non-Blocking이다.

- 하지만 프로그램이, 계속 직접 확인을 하고 있는 상태다.

recv()
 ↓
아직 없음
 ↓
recv()
 ↓
아직 없음
 ↓
recv()
 ↓
데이터 도착

- Non-Blocking이라는 것은 단지 함수가 완료될 때까지 현재 스레드를 붙잡고 있는가?를 보는 것이다.

- 그 자체가 작업 완료를 비동기적으로 통지받는가와는 별개 이야기다.


8. 동기/비동기와 Blocking/Non-Blocking의 차이

- 정리하면 기준을 다음과 같이 잡을 수 있다.

구분 확인할 내용
Synchronous / Asynchronous 작업 요청과 완료를 어떤 방식으로 처리하는가
Blocking / Non-Blocking 호출한 스레드가 작업 때문에 대기하는가

- 즉 둘은 서로 다른 관점의 개념이다.

- 이를 간단하게 표현하면 다음과 같다.

Sync / Async
     │
     └─ 작업 완료를 어떻게 다루는가?

Blocking / Non-Blocking
     │
     └─ 호출한 스레드가 기다리는가?

- 이 차이를 알아두는 것이 중요한 이유는 이후 네트워크 서버 구현에서 여러 I/O 모델을 접하게 되기 때문이다.


9. 네트워크 서버에서 생각해보기

- 가장 단순한 서버를 생각해보면 다음과 같이 구성할 수 있다.

while (true)
{
    SOCKET client = accept(listenSocket, ...);

    char buffer[1024];

    recv(client, buffer, sizeof(buffer), 0);

    ProcessPacket(buffer);
}

- 하지만 accept() 또는 recv()에서 작업이 완료되지 않는다면 해당 흐름은 그 작업을 기다리게 된다.

- 클라이언트가 증가하면 자연스럽게 다음 문제를 고민하게 된다.

Client A
    │
    └─ recv 대기

Client B
    │
    └─ 누가 처리하지?

Client C
    │
    └─ 누가 처리하지?

- 한 가지 방법으로 클라이언트마다 Thread를 둘 수도 있다.

Client A ─ Thread A
Client B ─ Thread B
Client C ─ Thread C
Client D ─ Thread D
...

- 구현은 직관적이지만 연결 수가 증가하면 스레드 수도 증가하게 된다.

- 결국 서버에서는 I/O를 기다리는 동안 Thread를 계속 점유해야 하는가?

- 이 문제를 해결하기 위해 Non-Blocking I/O, 이벤트 기반 처리, 비동기 I/O 등의 방식이 등장한다.

- 그리고 Windows에서는 이러한 비동기 I/O를 효율적으로 처리하기 위한 방식 중 하나로 I/O Completion Port(IOCP)를 제공한다.

- Windows 문서에서도 IOCP를 여러 비동기 I/O 요청을 처리하기 위한 효율적인 스레딩 모델로 설명한다.


10. 현재 프로젝트와 연결

- 현재 서버 프로젝트에서는 이후 IOCP를 이용한 네트워크 구조를 구현하려고 한다.

- IOCP 구조를 이해하려면 먼저 다음과 같은 사고 방식이 필요하다.

I/O 요청
   ↓
아직 작업이 완료되지 않음
   ↓
Thread는 다른 작업 수행
   ↓
I/O 완료
   ↓
완료된 I/O를 Worker Thread가 처리
Request
   ↓
Pending
   ↓
Completion

- 이와 같이 작업의 요청과 완료가 분리된다.

- 현재 구현하고 있는 TcpListener, 이후 구현하게 될 Session, Recv, Send, IocpContext 등의 구조 역시 이러한 흐름과 연결된다.

- 따라서 IOCP를 바로 보기 전에 동기/비동기와 Blocking/Non-Blocking을 구분해두는 것이 필요했다.


11. 정리

- 처음에는 동기와 Blocking, 비동기와 Non-Blocking을 같은 개념으로 생각하기 쉬웠다.

- 하지만 각각의 기준은 다르다.

동기 / 비동기 → 작업의 요청과 완료 처리를 바라보는 관점

Blocking / Non-Blocking → 함수를 호출한 Thread의 제어권을 바라보는 관점

- Blocking 방식은 구현이 간단하고 흐름을 이해하기 쉽지만, I/O가 오래 걸리는 경우 해당 스레드가 대기하게 된다.

- Non-Blocking 방식은 작업을 바로 완료할 수 없더라도 호출을 반환하기 때문에 스레드가 다른 작업을 수행할 수 있다. 대신 언제 다시 해당 작업을 처리할 수 있는지 알아내는 별도의 방법이 필요하다.

- 비동기 I/O에서는 작업 요청과 완료 처리를 분리하여 I/O가 진행되는 동안 호출 스레드가 다른 작업을 처리할 수 있다.

- 결국 네트워크 서버에서는 많은 연결의 I/O 대기 시간을 어떻게 처리할 것인지가 중요해지고, 이 문제는 이후 Overlapped I/O와 IOCP로 이어진다.

- 다음에는 Windows에서 비동기 I/O가 어떻게 이루어지는지 확인하면서 OVERLAPPED와 IOCP의 구조를 정리하려고 한다.


12. 참고

- https://learn.microsoft.com/en-us/windows/win32/fileio/synchronous-and-asynchronous-i-o

- https://learn.microsoft.com/en-us/windows/win32/winsock/nonblocking-i-o-2

 

Nonblocking Input/Output - Win32 apps

If a socket is in nonblocking mode, any I/O operation must either complete immediately or return the error code WSAEWOULDBLOCK indicating that the operation cannot be finished right away.

learn.microsoft.com

- https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-1-1-blocking-routines-and-einprogress-2

 

Windows Sockets 1.1 Blocking Routines and EINPROGRESS - Win32 apps

One major issue in porting applications from a Berkeley Sockets environment to a Windows environment involves blocking; that is, invoking a function that does not return until the associated operation is completed.

learn.microsoft.com

- https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-concepts

Comments