| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |
- 독서
- Online Judge
- Data Structure
- multi-thread
- Network Programming
- git
- Toy Project
- PS
- System Programming
- c#
- Unity
- design pattern
- BOJ
- team project
- Photon
- C++
- docker
- Today
- Total
I'm FanJae.
Unity 게임 개발 캠프 개인 프로젝트 3일차. 동기와 비동기, Blocking과 Non-Blocking (개인 공부 내용) 본문
Unity 게임 개발 캠프 개인 프로젝트 3일차. 동기와 비동기, Blocking과 Non-Blocking (개인 공부 내용)
FanJae 2026. 9. 16. 23:011. 시작에 앞서
- 네트워크 서버를 구현하다 보면 동기(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
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
'Projects > MyToyMapleServer' 카테고리의 다른 글
| Unity 게임 개발 캠프 개인 프로젝트 4일차. IOCP 기반 비동기 네트워크 처리 구조 구현 (0) | 2026.09.17 |
|---|---|
| Unity 게임 개발 캠프 개인 프로젝트 4일차. Blocking 서버의 한계 (개인 공부 내용) (0) | 2026.09.17 |
| Unity 게임 개발 캠프 개인 프로젝트 2일차. 인증 상태와 세션 전달 방식에 대한 고민 (0) | 2026.09.15 |
| Unity 게임 개발 캠프 개인 프로젝트 1일차. 서버 구조에 대한 고민 (0) | 2026.09.14 |
| Unity 게임 개발 캠프 개인 프로젝트 0일차. 기본 구상. (0) | 2026.09.13 |