동기/비동기 and 블로킹/논블로킹 이해
프로그래밍에서 동기(Synchronous)/비동기(Asynchronous)와 블로킹(Blocking)/논블로킹(Non-blocking)은 자주 혼동되는 개념입니다. 이 글에서는 이 두 가지 개념의 차이점을 알아보고, 파일 읽기 예시를 통해 어떻게 적용되는지 살펴보겠습니다.
동기(Synchronous)와 비동기(Asynchronous)
작업 간의 흐름과 완료 시점에 초점을 둡니다.
- 동기: 작업들이 순차적으로 진행되며, 완료 결과를 받아야 다음 작업이 시작됩니다.
- 비동기: 작업들이 동시에 또는 독립적으로 진행되며, 하나의 작업이 완료될 때까지 기다리지 않고 다음 작업을 시작합니다. 콜백(callback), 시그널(signal) 등을 통해 작업의 완료를 통지받습니다.
블로킹(Blocking)과 논블로킹(Non-blocking)
함수 호출 시 호출자의 대기 여부에 초점을 둡니다.
- 블로킹: 함수 호출 시 호출자는 작업이 완료될 때까지 제어권을 잃고 대기하는 상태가 됩니다. 그 동안 다른 작업을 수행하지 못합니다.
- 논블로킹: 함수 호출 시 바로 제어권을 반환받아 다른 작업을 수행할 수 있습니다. 호출자가 대기하지 않습니다.
동기/비동기와 블로킹/논블로킹 조합
동기 + 블로킹
동기(Synchronous)와 블로킹(Blocking)의 조합은, 함수가 호출되면 작업이 완료될 때까지 대기하고, 그 다음 코드가 실행되는 일반적인 함수 호출 방식입니다.
파이썬 코드 예시
def synchronous_blocking_read():
print("파일 읽기 시작")
with open('example.txt', 'r') as f:
data = f.read()
print("파일 읽기 완료")
print("데이터:", data)
print("메인 프로그램 시작")
synchronous_blocking_read()
print("메인 프로그램 종료")
Output:
메인 프로그램 시작
파일 읽기 시작
(파일 읽는 동안 대기)
파일 읽기 완료
데이터: (파일 내용 출력)
메인 프로그램 종료
코드 설명:
- open() 함수를 사용하여 파일을 열고 read() 메서드로 내용을 읽습니다.
- 파일을 모두 읽을 때까지 프로그램은 다음 단계로 진행하지 않고 대기합니다.
- 작업이 완료된 후에야 다음 코드가 실행됩니다.순차적으로 진행됩니다.
동기 + 논블로킹
주로 폴링(Polling) 기법을 사용하여 상태를 주기적으로 확인하는 방식에 사용됩니다. 호출자는 즉시 제어권을 반환받지만 작업은 순차적으로 진행됩니다.
파이썬 코드 예시
이 코드에서는 BlockingIOError가 발생하지 않을 수 있으며, 파일 시스템과 운영체제에 따라 다를 수 있습니다.
보통 소켓, 파이프와 같은 I/O 스트림에서 사용하는 것이 일반적이지만 이해를 돕기 위해 파일 read예시로 코드를 작성했습니다.
import os
import errno
import time
def synchronous_nonblocking_read():
print("파일 읽기 시작")
fd = os.open('example.txt', os.O_RDONLY | os.O_NONBLOCK)
data = b''
while True:
try:
chunk = os.read(fd, 1024)
if not chunk:
break
data += chunk
except BlockingIOError as e:
if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK):
print("데이터를 기다리는 중...")
time.sleep(0.1) # 잠시 대기 후 다시 시도
continue
else:
raise
os.close(fd)
print("파일 읽기 완료")
print("데이터:", data.decode())
print("메인 프로그램 시작")
synchronous_nonblocking_read()
print("메인 프로그램 종료")
Output:
메인 프로그램 시작
파일 읽기 시작
데이터를 기다리는 중...
데이터를 기다리는 중...
데이터를 기다리는 중...
파일 읽기 완료
데이터: (파일 내용 출력)
메인 프로그램 종료
코드 설명:
- os.open() 함수를 사용하여 파일을 논블로킹 모드(os.O_NONBLOCK)로 엽니다.
- os.read()를 통해 파일을 읽으려 하지만, 데이터가 준비되지 않았다면 BlockingIOError 예외가 발생합니다.
- 예외 처리에서 EAGAIN, EWOULDBLOCK 오류 코드를 확인하여 데이터가 준비될 때까지 루프를 돌며 재시도합니다.
- 파일을 모두 읽으면 루프를 종료하고 파일을 닫습니다.
비동기 + 블로킹
작업을 비동기로 호출했지만, 완료될 때까지 기다립니다. 비동기의 장점을 살리지 못하고 블로킹되기 때문에 비효율적입니다.
비동기 + 논블로킹
작업을 비동기로 호출하고, 호출자는 즉시 제어권을 반환받아 다른 작업을 수행합니다. 작업이 진행되는 동안 다른 작업을 수행할 수 있어 자원 활용에 효율적입니다.
파이썬 코드 예시
import asyncio
import aiofiles
async def asynchronous_nonblocking_read():
print("파일 읽기 시작")
async with aiofiles.open('example.txt', 'r') as f:
data = await f.read()
print("파일 읽기 완료")
print("데이터:", data)
async def main():
print("메인 프로그램 시작")
task = asyncio.create_task(asynchronous_nonblocking_read())
print("다른 작업 수행 중...")
await task # 파일 읽기가 완료될 때까지 대기
print("메인 프로그램 종료")
asyncio.run(main())
Output:
메인 프로그램 시작
다른 작업 수행 중...
파일 읽기 시작
(파일 읽는 동안에도 다른 작업 가능)
파일 읽기 완료
데이터: (파일 내용 출력)
메인 프로그램 종료
코드 설명:
- aiofiles.open()을 사용하여 비동기적으로 파일을 엽니다.
- await f.read()로 파일을 읽는 동안에도 이벤트 루프는 다른 작업을 수행할 수 있습니다.
- asyncio.create_task()를 통해 작업을 스케줄링하고, 즉시 제어권을 반환합니다.
- 파일 읽기가 진행되는 동안 “다른 작업 수행 중…“이 출력됩니다.
개념 정리
동기 vs 비동기
- 동기는 작업의 순서와 의존성에 초점을 둡니다.
- 비동기는 작업이 독립적으로 진행될 수 있음을 나타냅니다.
블로킹 vs 논블로킹
- 블로킹은 호출한 측에서 기다리는지 여부를 나타냅니다.
- 논블로킹은 호출한 측에서 즉시 제어권을 반환받는지 여부를 나타냅니다.
언제 어떤 방식을 사용할까?
- 동기 블로킹: 구현이 비교적 쉽기 때문에 간단한 스크립트나 순차적 작업에 적합합니다.
- 동기 논블로킹: 비동기 처리를 도입하기 어려운 경우에 사용됩니다. 주로 폴링에 사용됩니다.
- 비동기 블로킹: 비효율적인 방식이라 거의 사용하지 않습니다.
- 비동기 논블로킹: 대규모 네트워크 요청이나 입출력 작업에서 성능 향상을 위해 사용됩니다.
결론
동기/비동기와 블로킹/논블로킹은 프로그래밍에서 중요한 개념으로, 시스템의 성능과 효율성에 큰 영향을 미칩니다.
각 개념을 올바르게 이해하고 상황에 맞게 적용하면, 프로그램의 응답성과 자원 활용을 최적화할 수 있습니다.