헬창 개발자
Sync/Async & Blocking/Non-blocing 본문
Blocking & Non-blocking 이란 무엇일까?
동기/비동기에 들어가기 전 먼저 필수적으로 짚고 넘어가야 할 개념이 바로 Blocking 과 Non-blocking 개념입니다. 이 부분은 관점을 제어로 바라보는 부분입니다. CPU에 대한 제어권을 넘겨주지 않는다면 Blocking 이며, 넘겨준다면 Non-blocking 입니다.
보통 이 Non-blocking을 수행할 때는 I/O를 빼먹을 수 없는데요. 이것이 대표적인 Non-blocking의 사례이기 때문입니다. I/O 처리는 굉장히 무겁고 시간이 걸리는 작업이기 때문에, 이를 요청했던 작업이 이 I/O처리가 다 될때까지 기다리는 것이 굉장히 비효율적이게 됩니다. 그래서 Non-blocking으로 I/O 처리를 해주어 작업을 계속하다가 커널 영역에서 I/O 처리가 끝나면 Interrupt를 하는 식으로 진행하는 것이죠.
- Blocking: 작업 A가 작업 B를 요청했을 때, B가 끝날 때까지 A는 기다림 (멈춤)
- Non-blocking: 작업 A가 작업 B를 요청했어도, A는 계속 다른 일 수행 가능
- #기술적 포인트
- 보통 I/O 처리(파일 읽기, 네트워크 요청 등)에서 Non-blocking이 중요함
- I/O는 느리기 때문에, Non-blocking으로 CPU 낭비 없이 효율 처리
- 커널이 I/O 완료되면 Interrupt로 알려줌
Sync(동기)/Async(비동기)란 무엇일까?
개념 요약
- Synchronous (동기): 작업 요청 → 결과 받을 때까지 기다림
- Asynchronous (비동기): 작업 요청 → 결과 안 기다리고 다른 일 수행
핵심 차이: "응답 순서"
- 동기: 요청한 순서대로 응답도 옴
- 비동기: 요청한 순서와 응답 순서가 달라질 수 있음
4가지 조합
정리하자면 Blocking/Non-blocking 은 제어에 관점을 두는 것이고 Sync/Async 는 순서에 관점을 두는 것입니다. 이를 통해서 총 4가지의 조합이 나올 수 있는데요. 하나씩 알아보도록 합시다. 먼저 Sync 를 기준으로 알아보도록 하겠습니다.
1️⃣ Sync + Blocking
가장 전통적인 방식
- A가 B 작업을 요청하면, B는 완료될 때까지 A를 막고(block) 제어권을 돌려주지 않음
- A는 아무것도 못 하고 기다려야만 함
- B가 완료되면 그제서야 응답과 함께 A가 다시 실행됨
- 대부분의 기본 코드(파일 읽기, 네트워크 요청 등)는 이 방식으로 동작함
2️⃣ Sync + Non-blocking
제어는 자유지서는 지켜야 하는 상황
- A가 B 작업을 요청하면, B는 즉시 제어권을 A에게 돌려줌 (Non-blocking)
- 하지만 A는 결과가 꼭 필요하기 때문에 반복해서 결과를 체크함
- 이 방식에서 쓰는 대표적인 기법은 Polling
- 제어는 자유롭지만, 여전히 순서에 묶여 있기 때문에 병렬성이 제한됨
while not b.is_done():
sleep(0.1) # 계속 확인해야 함 (Polling)
result = b.get_result()
3️⃣ Async + Blocking
이론적으론 가능하나, 실용적 가치 없음
- 비동기(async) 구조지만, B 작업이 A를 막아버림(blocking)
- 순서 자유로움의 장점을 전혀 살리지 못함
- 실무에서는 의도치 않게 이 구조가 만들어지는 경우가 많음 (예: 비동기 함수 안에서 동기 blocking 호출)
async def fetch_data():
data = requests.get("http://example.com") # 동기 blocking I/O → async 의미 없어짐
4️⃣ Async + Non-blocking
- A는 여러 작업을 요청하면서 각 요청에 얽매이지 않음
- 요청은 Non-blocking으로 처리되며, 결과는 나중에 도착
- 응답은 Callback, Promise, 혹은 async/await으로 처리
- 자원 효율적이고, 고성능 시스템에 적합
- 다만 구조가 복잡해질 수 있고, 관리 어려움 (특히 콜백 지옥)
// JavaScript 예시
// 1. 콜백 지옥
fs.readFile('file.txt', function (err, data) {
doSomething(data);
});
// 2. Promise
readFile('file.txt')
.then(data => doSomething(data))
.catch(err => handleError(err));
// 3. async/await
async function main() {
try {
const data = await readFile('file.txt');
doSomething(data);
} catch (err) {
handleError(err);
}
}
🤔 무조건 Async + Non-blocking이 답일까?
비동기 + Non-blocking은 자원을 효율적으로 사용하고, 실제 처리 시간을 단축시키는 이상적인 조합입니다. 하지만, 모든 상황에서 무조건 비동기만 고집할 수는 없습니다.
비동기보다 동기가 더 적합한 경우도 있다!
예를 들어볼게요.
당신의 계좌에 10,000원이 있습니다.
그리고 8,000원짜리 국밥, 5,000원짜리 담배를 사고 싶어요.
이 결제 로직을 비동기로 처리하면 어떻게 될까요?
- 국밥 결제 요청 → 비동기 처리
- 담배 결제 요청 → 동시에 비동기 처리
그러면 둘 다 성공해버릴 수도 있습니다.
국밥 결제가 완료되기 전이라면 잔액은 여전히 10,000원이니까요.
이건 말이 안 되죠. 잔액이 2,000원밖에 안 남았는데 담배를 살 수 있다니...?
이처럼 이전 작업의 결과에 따라 다음 작업이 결정되는 상황에선 비동기가 아니라 동기 + Blocking이 필요합니다.
즉, "순서와 무결성"이 중요한 상황에선 반드시 동기 처리를 고려해야 합니다.
실제 시스템은 어떻게 대응할까?
이런 문제는 실무에선 보통 트랜잭션이나 락(Lock)을 통해 해결합니다.
- 잔액과 같은 중요한 데이터는 Critical Section(임계 영역)으로 관리되어야 하며,
- 동시에 접근하면 안 되기 때문에 Mutex, Semaphore, Lock 등을 통해 동기적으로 처리합니다.
#부록: 파이썬에서의 비동기 처리
Python에선 Thread나 Process가 실행 흐름의 단위
- 파이썬에서 병렬 작업을 하려면 Thread 또는 Process를 사용해야 합니다.
- 각각은 threading, multiprocessing 모듈로 지원됩니다.
그런데...
CPython에는 GIL(Global Interpreter Lock) 이 있습니다.
🔒 GIL: 파이썬 인터프리터는 한 번에 하나의 스레드만 실행 가능하게 강제합니다.
그래서 threading 모듈을 써서 여러 스레드를 만들어도 진짜 병렬 처리는 되지 않습니다.
CPU 코어를 여러 개 쓴다기보단, 한 코어에서 컨텍스트 스위칭만 열심히 반복하는 상황이 됩니다.
하지만 CPU-bound 작업이 아닌 I/O-bound 작업인 경우, 여전히 스레드 기반의 병렬 처리 모델이 효과적일 수 있습니다. I/O-bound 작업은 CPU가 아닌, 파일의 경우 저장장치 디바이스에서, 네트워크의 경우 네트워크 인터페이스 카드(NIC)에서, 쿼리의 경우 데이터베이스에서 수행하기 떄문이죠. 스레딩을 통해 Non-blocking을 유도하여 충분히 동시적으로 수행할 수 있는 겁니다.
그런데 Python 에서 개발자가 직접이 스레드를 관리하는 것은 여간 복잡하고 어려운 작업이 아닐 수 없습니다. 그래서 3.4 버전부터 Python에서는 I/O 작업에 대한 비동기 프로그래밍을 효율적으로 지원하기 위한 asyncio 라이브러리를 비동기 표준 라이브러리로써 지원하고 있습니다. 이 친구를 중심으로 알아보도록 합시다!
asyncio
asyncio는 Coroutine과 Task를 동작시키기 위한 high-level의 API들을 의미합니다. 먼저, Coroutine에 대해 알아봅시다.
#코루틴(Coroutine)이란?
스레드보다 가볍고, 함수보다 유연한 비동기 단위
1️⃣ 서브루틴 vs 코루틴
서브루틴 (Subroutine)
- 일반적인 함수
- 시작 → 끝까지 한 번에 실행됨
- 중간에 멈췄다 재개 불가
코루틴 (Coroutine)
- 중간에 멈췄다가 다시 실행 가능
- 더 일반화된 함수 구조
- 실행 중에 await로 다른 작업을 양보할 수 있음
2️⃣ 코루틴의 가벼움 = 성능 향상
- 코루틴은 스레드보다도 훨씬 가벼움
- OS가 아닌 언어(Python) 수준에서 관리됨
- 실행/중지/삭제가 빠름 → 메모리 낭비 적고, 전환 비용 낮음
- 한 개의 스레드 안에서도 여러 코루틴을 동시에 관리 가능
👉 하나의 스레드에서 수천 개의 코루틴을 동시 처리할 수 있음 (특히 I/O-bound에 효과적)
3️⃣ 파이썬에서 코루틴 = async def + await
import asyncio
async def main():
print('hello')
await asyncio.sleep(1)
print('world')
asyncio.run(main())
- async def: 코루틴 정의
- await: 다른 코루틴이 끝날 때까지 잠깐 양보
- asyncio.run(): 이벤트 루프를 돌려 실행
결과:
hello
(1초 대기)
world
asyncio.sleep(1)은 코루틴을 멈추고, 다른 코루틴에게 CPU를 양보하는 대표적인 예
4️⃣ 이벤트 루프(Event Loop)
- asyncio의 코루틴 실행 관리 주체
- 하나의 스레드에 하나만 존재해야 함
- asyncio.run()은 이벤트 루프 생성 → 메인 코루틴 실행 → 종료
- 루프가 돌면서 await한 코루틴을 스케줄링함
✔ run()은 한 번만 호출
✔ 다른 루프가 돌아가는 중이면 오류 발생
5️⃣ 그런데... 동시성 안 되는 예시 😅
async def main():
await say_after(1, 'hello') # 1초 기다림
await say_after(2, 'world') # 그 후 2초 기다림
결과:
started at 15:54:20
hello
world
finished at 15:54:23
문제점:
- 동시 실행 아님. 순차적 실행
- 코루틴이지만 직렬(await → await) 구조라 총 3초
6️⃣ 동시 실행하려면? → asyncio.create_task()
async def main():
task1 = asyncio.create_task(say_after(1, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
await task1
await task2
결과:
started at 16:29:20
hello
world
finished at 16:29:22
요약:
- create_task()는 코루틴을 Task 객체로 감싸고 이벤트 루프에 등록
- 동시에 실행되도록 스케줄링됨
7️⃣ 최신 방식: TaskGroup (Python 3.11+)
async def main():
async with asyncio.TaskGroup() as tg:
tg.create_task(say_after(1, 'hello'))
tg.create_task(say_after(2, 'world'))
- 코드 간결, 오류 전파 잘 됨
- 여러 Task를 묶고 모두 끝날 때까지 기다림
8️⃣ 일반 함수도 async로 돌리고 싶다면?
- 일반 함수는 await 불가능
- 그래서 asyncio.to_thread() 사용
import asyncio
def blocking_func():
return sum(i for i in range(100_000))
async def main():
result = await asyncio.to_thread(blocking_func)
print(result)
9️⃣ 실무 예시: Starlette
- FastAPI, Starlette 등에서는 경로 함수가 async def면 코루틴, def면 스레드풀 사용
- 백그라운드 작업도 동일하게 비동기 실행
- 내부적으로 이벤트 루프 기반으로 처리
파이썬의 웹 프레임워크인 Starlette 에서 위의 내용을 확인할 수 있습니다. 경로로 지정되어 호출되는 함수가 async def 라면 코루틴을 통해서, def 라면 외부의 스레드 풀을 사용해서 작업을 처리하고 이를 await 하죠. 먼저 응답을 보내고 실행하기 위한 Background tasks도 마찬가지 입니다.
Concurrency & Parallelism
여기서 한 번 짚고 넘어가야 할 부분이 바로 동시성과 병렬성 의 차이입니다. Asynchronous 라는 개념은 엄밀히 따지면 동시성이라고 합니다. 동시성과 병렬성 모두 서로 다른 일들이 거의 같은 시간 내에 일어난다는 개념 때문에 완전히 같다고 생각하기 일쑤입니다.
용어 정의
동시성 (Concurrency) | 논리적으로 여러 작업이 겹치는 시점에 실행되는 것 (진짜 동시에 아니어도 됨) |
병렬성 (Parallelism) | 물리적으로 여러 작업이 정확히 같은 시간에 동시에 실행되는 것 |
차이점 요약
초점 | 작업 간의 시간 겹침 | 동시 실행 자체 |
실행 환경 | 싱글 코어에서도 가능 | 멀티 코어/멀티 프로세서 필요 |
예시 | A → B → A 식으로 교차 처리 (중간중간 전환) | A + B가 진짜로 동시에 실행됨 |
스케줄링 | 개발자 또는 이벤트 루프가 관리 | OS 스케줄러가 관리 (멀티스레드, 멀티프로세스) |
관계 | 병렬성을 포함할 수 있음 | 동시성을 전제하지 않아도 병렬 처리는 가능 |
먼저 더 간단한 병렬성은 말 그대로 여러 개의 작업들이 진짜로 같은 시간에 돌아가는 것을 뜻합니다. 컴퓨터에서 이를 달성하기 위해서는 여러 개의 코어, 즉 프로세서가 있어야 합니다. 하지만 동시성은 꼭 같은 시간일 필요는 없습니다. 동시성은 작업의 시작과 종료에 걸치는 기간이 겹치는 것에 초점을 맞춥니다. 예를 들어 만약 프로세서가 하나라면 하나에서 여러 작업들이 이루어져야 하죠. 만약 A가 끝나고 B가 끝난다면 이는 동시성을 만족하지 못합니다. 하지만 A를 진행하다 잠시 멈추고 B를 진행한다음 다시 A로 돌아와 작업을 마친다면 이는 동시성을 만족한 것입니다. 물론 A가 멈출 필요 없이 병렬적으로 진행된다면 더욱 빠르고 효과적일 것이고, 이것이 FastAPI에서 병렬적이지 않은 NodeJS보다 높은 성능을 낸다고 자부하는 이유 중에 하나입니다.
쉬운 비유
동시성 (Concurrency)
👉 혼자서 두 명의 친구에게 동시에 카톡 답장을 하는 것
(한 명에게 답하고, 잠깐 멈췄다가 다른 사람에게 답하고 다시 돌아옴)
병렬성 (Parallelism)
👉 친구 두 명에게 동시에 각기 다른 폰으로 답장 보내는 것
(진짜 동시에 처리됨, 물리적 분리)
코루틴은 병렬이 아닌 동시성을 위한 도구이며,
하나의 스레드 안에서 협조적 스케줄링으로 효율적인 멀티태스킹을 구현하는 가장 아름다운 방법입니다.😉
🔟 await의 뿌리는 yield!?
yield는 코루틴 개념의 기초가 되는 키워드입니다.
코루틴이 나오기 전, 제너레이터(Generator) 를 통해 먼저 도입되었죠.
yield란?
- 함수 실행을 일시 정지하고, 값을 하나씩 반환하는 키워드
- 일반 함수가 아니라 제너레이터(generator) 를 생성함
def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
gen = count_up_to(3)
print(next(gen)) # 1
print(next(gen)) # 2
작동 원리
- yield가 호출되면 현재 스택 프레임이 보존됨 → 다음 next() 시점부터 이어서 실행
- 즉, 일시 정지 + 상태 유지 = 코루틴의 기초적인 형태
yield vs return
항목 | yield | return |
반환 | 값을 하나씩 반환 (반복 가능) | 값을 한 번에 반환 |
상태 유지 | 가능 (함수 일시 정지 & 이어 실행) | 불가 (함수 종료) |
메모리 효율 | 매우 좋음 (지연 평가 / lazy eval) | 전체 메모리 필요 |
주 용도 | 반복자, 스트리밍 처리, 비동기 흐름 제어 등 | 단일 결과 반환 |
참고자료
https://seungriyou.github.io/posts/python-global-interpreter-lock/
https://docs.python.org/3/glossary.html#term-coroutine
https://docs.python.org/3/library/asyncio-task.html#task-groups
https://velog.io/@richpin/%EB%8F%99%EA%B8%B0%EB%B9%84%EB%8F%99%EA%B8%B0-feat.-Python
https://wikidocs.net/229721
https://hwangheek.github.io/2019/asynchronous-python/
'공부방' 카테고리의 다른 글
Hash Ring (1) | 2025.07.03 |
---|---|
종속성 관리 - 서브모듈, 서브트리 (0) | 2025.06.17 |
메시지 브로커(Redis)와 Celery (0) | 2025.05.29 |
docker health check (0) | 2025.05.09 |
논문 리뷰: s1: Simple test-time scaling (1) | 2025.02.18 |