CS/Operating System

Sync & Async과 Blocking I/O & Non Blocking I/O

jinsang-2 2025. 3. 5. 14:56

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. 팀장 : 사원1씨 A업무좀 해주세요. (동시에 지시)
  2. 팀장 : 사원2씨 B업무좀 해주세요. (동시에 지시)
  3. 팀장 : 사원3씨 C업무좀 해주세요. (동시에 지시)
  4. 팀장 : 다른일을 해야지 ~
  5. 사원2 : 팀장님 B 모두 처리했습니다. (업무량에 따라 각 사원마다 완료하는 시간이 제각기 다를 수 있다)
  6. 사원1 : 팀장님 A 모두 처리했습니다.
  7. 사원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
'''