I/O 종류
- 네트워크 (socket)
- file
- pipe (프로세스 간)
- device (키보드와 같은 장치 등등)

Sync
- Synchronous : 동기
- 모든 요청과 응답이 일련의 순서를 따른다.
- 작업의 완료 여부를 호출한 측에서 직접 확인하고, 작업이 끝난 후에 다음 작업을 수행함.
Async
- Asynchronous : 비동기
- 작업을 요청한 후 즉시 다음 작업을 수행할 수 있으며, 작업이 완료되면 별도의 콜백 함수나 이벤트를 통해 결과를 확인함.
비동기 코드 예제
import asyncio
async def async_order1():
print("task1 주문을 처리 중... ")
await asyncio.sleep(2) # 2초 대기 (하지만 CPU는 다른 작업 수행 가능)
return "아메리카노"
async def async_order2():
print("task2 주문을 처리 중... ")
await asyncio.sleep(2) # 2초 대기 (하지만 CPU는 다른 작업 수행 가능)
return "카페모카"
async def main():
print("<주문 요청>")
# 비동기 작업을 시작하지만 기다리지 않음
task1 = asyncio.create_task(async_order1())
task2 = asyncio.create_task(async_order2())
print("---다른 작업 수행 중...---") # ✅ 주문이 처리되는 동안 다른 작업 가능
result1 = await task1 # 주문이 끝날 때까지 기다림
result2 = await task2
print(f"task1 주문 완료: {result1}")
print(f"task2 주문 완료: {result2}")
# 이벤트 루프 실행
asyncio.run(main())
✅ 출력 결과
order1의 sleep(2) + order2의 sleep(2) = 4초가 아닌 2초만 대기하면 된다!
<주문 요청>
---다른 작업 수행 중...---
task1 주문을 처리 중...
task2 주문을 처리 중...
(2초 대기)
task1 주문 완료: 아메리카노
task2 주문 완료: 카페모카
Blocking I/O
Blocking I/O란 호출한 함수가 작업을 완료할 때까지 반환(return)되지 않고, 현재 실행 중인 스레드가 해당 작업이 끝날 때까지 대기하는 방식을 의미한다.
- 예를 들어 아래와 같이 Read()라는 시스템 콜을 요청했을 때 Read() 가 response 될 때까지 기다리고 있는 것

Non-Blocking I/O
호출한 함수가 “즉시 반환(return)”되며, 작업이 완료되지 않아도 다른 작업을 수행할 수 있다.
- read I/O를 하기 위해 시스템 콜을 수행하면, 커널의 I/O 작업 완료 여부와는 무관하게 즉시 응답한다.
- 즉시 return : -1 (EAGAIN, EWOULDBLOCK)
- 즉시 응답하기에 read I/O 가 완료되었는지 모르기에 폴링(Polling)방식이나 이벤트 기반(Event-Driven) 방식으로 확인한다.
- 폴링 방식 : 주기적으로 read() 작업이 완료 되었는지 확인하는 방식
- 이벤트 기반 방식 : 이벤트가 발생할 때마다 호출되는 콜백 함수나 이벤트 루프를 퉁해 작업이 완료되었는지 확인.
- 커널(Kernel)이 시스템 콜을 받자마자 CPU 제어권을 다시 어플리케이션(Application)에게 넘겨주고, I/O 작업이 완료되기 전에 다른 작업을 수행할 수 있다.
- 어플리케이션 다른 작업들을 수행하다가 중간중간 시스템 콜을 보내서 I/O가 완료됐는지 커널에게 물어보고, 완료되면 I/O 작업을 완료한다.

차이점 정리
- 동기(Synchronous) vs 비동기(Asynchronous)
- 동기: 작업이 끝날 때까지 기다림, 한 번에 하나의 작업만 처리.
- 비동기: 작업이 끝날 때까지 기다리지 않고 다른 작업을 동시에 진행할 수 있음.
- 블로킹(Blocking) vs 논블로킹(Non-blocking)
- 블로킹: 작업이 완료될 때까지 현재 작업이 멈추고 기다림.
- 논블로킹: 작업을 요청한 후 즉시 반환하고, 작업이 완료되지 않아도 다른 일을 할 수 있음.
개념 조합

구분 설명
Blocking + Synchronous | 요청한 작업이 끝날 때까지 현재 스레드가 대기하며, 결과를 직접 받아 처리함. |
Blocking + Asynchronous | 요청한 작업이 끝날 때까지 대기하지만, 완료 후 콜백 등을 활용하여 결과를 처리함. |
Non-Blocking + Synchronous | 요청한 작업이 즉시 반환되지만, 작업 완료 여부를 주기적으로 확인(polling)하여 처리함. |
Non-Blocking + Asynchronous | 요청한 작업이 즉시 반환되며, 작업 완료 후 콜백이나 이벤트를 통해 결과를 처리함. |
1. Sync + Blocking

sync blocking은 I/O가 실행되는 동안 어플리케이션이 다른 일은 못하고 Read만 수행
- blocking : I/O 호출이 발생햇을 때 커널의 I/O 작업이 완료될 때까지 제어권을 커널에서 가지고 있기 때문에, 유저 프로세스는 I/O가 완료되기 전에 다른 작업 할 수 없음
- Sync : 작업이 완료되면 해당 작업 결과를 가지고 어플리케이션에서 직접 처리
sync + blocking은 I/O가 실행되는 동안 어플리케이션이 다른 일은 못하고 Read만 수행
- user space에 존재하는 process는 kernel에게 I/O 요청하는 함수를 system call 한 뒤 kernel이 작업 결과를 반환하기까지 중단된 채 대기(block)
- 호출할 때마다 요청 thread를 생성하므로 I/O 요청 수가 많아진다면 한 작업 당 한 번의 컨텍스트 스위칭이 발생하기에 비효율적이다. 또한 I/O 작업을 기다리느라 CPU 자원이 놀고 있음
코드 예시
import time
def blocking_sync():
print("작업 시작")
time.sleep(2) # 2초 동안 블로킹
print("작업 완료")
blocking_sync()
print("다음 작업 실행")
설명: sleep(2)는 호출한 스레드를 2초 동안 블로킹합니다. 즉, 함수가 실행되는 동안 다른 작업을 수행할 수 없습니다.
2. Asynchronous + Blocking (비동기 + 블로킹)

- 위 그림은 I/O 작업 자체에 block되는 것이 아니라, select, poll과 같은 i/o 멀티플렉싱관련 system call에 대한 kernel의 응답이 block된다 생각하면 됨
- select() : 여러 파일 디스크립터(소켓, 파일)에서 읽기, 쓰기, 예외 발생 여부를 확인하고, 어떤 파일 디스크립터가 준비되었는지 알려주는 역할을 한다. 한 번에 여러 i/o 작업을 처리할 수 있게 해줌. 지정된 시간 동안 기다리고 그 시간 내에 준비된 파일 디스크립터(소켓, 파일)만 반환함
3. Synchronous + Non-Blocking (동기 + 논블로킹)

non-blocking
- I/O 호출이 발생했을 때 커널의 I/O 작업 완료 여부와는 무관하게 즉시 응답한다.
- 커널이 시스템 콜을 받자마자 제어권을 다시 App에 넘겨주기 때문에, 유저 프로세스는 I/O가 완료되기 전에 다른 작업을 할 수 있다.
sync
- 다른 작업을 수행하다가 중간중간에 시스템 콜을 보내 I/O 작업이 완료됐는지 확인
- I/O작업이 처리됐을 때의 결과를 호출한 함수에서 처리한다. 직접 결과를 처리해야 하기 때문에 지속적으로 I/O 종료를 물어보는 것도 이 때문이다.
동기식으로 동작하기에 user process는 여전히 I/O 완료만 기다리며 컨텍스트 스위칭이 빈번하게 일어나는 구조
- 제어권을 반환 받고 다른 작업도 처리할 수 있지만, 계속해서 원하는 결과를 반환받기까지 계속 상태 체크를 해야한다.
- 적정한 polling 주기가 필요한데 주기가 너무 길어질 경우 실제 데이터는 다 준비 되었지만 후속 처리가 늦어질 수 있고, 주기가 짧다면 쓸데 없는 컨텍스트 스위칭만 발생하고, kernel 입장에서 의미 없는 return을 자주 해줘야 하기에 오히려 I/O 작업의 지연이 초래된다.
#include <stdio.h>
#include <unistd.h>
int is_done = 0;
void nonblocking_sync() {
printf("작업 시작\\n");
int count = 0;
while (count < 20) { // 폴링 방식
usleep(100000); // 0.1초 대기
count++;
if (count == 20) {
is_done = 1;
}
}
printf("작업 완료\\n");
}
int main() {
nonblocking_sync();
printf("다음 작업 실행\\n");
return 0;
}
설명: 작업을 진행하면서 while 루프를 통해 주기적으로 상태를 확인(polling)하는 방식입니다. 작업이 완료될 때까지 계속 확인하며, 다른 작업을 수행하지 않습니다.
Asynchronous + Non-Blocking(비동기 + 논블로킹)
- 팀장 : 사원1씨 A업무좀 해주세요. (동시에 지시)
- 팀장 : 사원2씨 B업무좀 해주세요. (동시에 지시)
- 팀장 : 사원3씨 C업무좀 해주세요. (동시에 지시)
- 팀장 : 다른일을 해야지 ~
- 사원2 : 팀장님 B 모두 처리했습니다. (업무량에 따라 각 사원마다 완료하는 시간이 제각기 다를 수 있다)
- 사원1 : 팀장님 A 모두 처리했습니다.
- 사원3 : 팀장님 C 모두 처리했습니다.

I/O 작업이 완료될 때까지 기다리지 않고 다른 작업을 계속할 수 있는 방식이다. 이 방식에서는 비동기적으로 작업을 처리하고, 논블로킹 방식으로 I/O를 처리하므로, I/O가 완료되지 않더라도 다른 작업을 계속해서 처리할 수 있다.
import asyncio
# 비동기적으로 파일 읽기 함수
async def read_file(file_name):
print(f"Start reading {file_name}")
# 비동기적으로 파일을 읽음 (I/O 작업)
await asyncio.to_thread(read_file_sync, file_name)
print(f"Finished reading {file_name}")
# 동기적으로 파일을 읽는 함수 (단, 비동기 방식에서는 이 함수를 to_thread로 감싸서 사용)
def read_file_sync(file_name):
with open(file_name, 'r', encoding='utf-8') as f:
data = f.read()
return data
# 여러 파일을 비동기적으로 읽는 메인 함수
async def main():
# 비동기적으로 파일 읽기 요청 (파일 읽기 작업이 동시에 진행됨)
task1 = asyncio.create_task(read_file('file1.txt'))
task2 = asyncio.create_task(read_file('file2.txt'))
task3 = asyncio.create_task(read_file('file3.txt'))
# 비동기 작업들이 완료될 때까지 기다리기
await task1
await task2
await task3
# 이벤트 루프 실행
asyncio.run(main())
# file1이 가장 오래걸릴 때 출력 결과
'''
Start reading file1.txt
Start reading file2.txt
Start reading file3.txt
Finished reading file2.txt
Finished reading file3.txt
Finished reading file1.txt
'''
'CS > Operating System' 카테고리의 다른 글
멀티 프로그래밍, 멀티 태스킹, 멀티 프로세싱, 멀티 스레딩 (0) | 2025.01.23 |
---|---|
교착 상태(Deadlock) 및 은행원 알고리즘(Banker’s Algorithm) (1) | 2025.01.15 |
Critical Section / 세마포어(Semaphore) / 뮤텍스(Mutex) (0) | 2025.01.15 |