ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] 왜 Asyncio에는 Executor가 필요한가 — Async와 Cpu-bound의 경계
    IT 2026. 1. 23. 11:35

    async와 CPU-bound의 경계

    async / await를 어느 정도 이해하고 나면, 결국 이런 질문에 도달하게 됩니다.

    async로 작성했는데, 왜 어떤 코드는 여전히 느릴까?

    그리고 조금 더 파고들면, run_in_executor 혹은 ThreadPoolExecutor, ProcessPoolExecutor 같은 존재를 만나게 됩니다. 이 글에서는 executor를 문법이나 사용법이 아니라, 왜 asyncio 세계에 이런 장치가 필요했는지라는 관점에서 정리해보려 합니다.


    async는 모든 문제를 해결하지 않는다

    async는 강력하지만, 만능은 아닙니다. async가 잘 해결하는 문제는 명확합니다.

    • 네트워크 I/O
    • 파일 I/O
    • 대기 시간이 많은 작업

    이들의 공통점은 CPU를 거의 사용하지 않는다는 점입니다. async는 이 대기 시간을 효율적으로 숨기는 구조이지, CPU 연산을 빠르게 만드는 기술이 아닙니다.


    CPU-bound 작업의 등장

    다음과 같은 코드를 생각해봅시다.

    async def calc():
        total = 0
        for i in range(10**8):
            total += i
        return total

    문법적으로는 async 함수지만, 이 코드는 전혀 비동기적이지 않습니다. await도 없고, 실행 중에 제어권을 넘길 수도 없습니다.

    이 함수가 실행되는 동안 이벤트 루프는 완전히 멈춥니다.


    이벤트 루프의 한계

    이벤트 루프는 다음과 같은 전제를 가지고 설계되었습니다.

    • 작업은 짧게 실행된다
    • 오래 걸리는 작업은 스스로 양보한다

    CPU-bound 작업은 이 전제를 정면으로 깨뜨립니다. 그래서 asyncio는 이런 작업을 이벤트 루프 안에서 처리하지 않도록, 의도적으로 도망갈 출구를 만들어두었습니다. 그것이 바로 executor입니다.


    executor란 무엇인가

    executor는 한 문장으로 이렇게 정리할 수 있습니다.

    이벤트 루프 밖에서 실행되는 작업 공간

    즉,

    • 이벤트 루프는 계속 돌고
    • 무거운 작업은 다른 스레드나 프로세스에서 실행되며
    • 결과만 다시 이벤트 루프로 돌아옵니다

    이 구조 덕분에 async 프로그램은 멈추지 않습니다.


    run_in_executor의 의미

    loop.run_in_executor(None, blocking_func)

    이 코드는 단순히 “스레드에서 실행해라”라는 명령이 아닙니다. 의미를 풀면 이렇습니다.

    • 이 함수는 오래 걸릴 것이다
    • 이벤트 루프를 막지 않게 해달라
    • 끝나면 결과만 알려달라

    이것은 성능 최적화라기보다 이벤트 루프를 보호하기 위한 장치입니다.


    Thread vs Process

    executor에는 두 가지 선택지가 있습니다.

    ThreadPoolExecutor

    • I/O 성격의 블로킹 코드에 적합
    • GIL의 영향을 받음
    • 컨텍스트 스위칭 비용이 낮음

    ProcessPoolExecutor

    • CPU-bound 작업에 적합
    • GIL을 우회 가능
    • 프로세스 간 통신 비용 존재

    여기서 중요한 점은, async 자체가 이 둘을 대체하는 기술이 아니라는 사실입니다. async는 이들을 조율하는 역할을 합니다.


    async의 진짜 역할

    이쯤 오면 async의 정체가 꽤 분명해집니다.

    • async는 빠르게 계산하기 위한 도구가 아니라
    • 기다림을 효율적으로 관리하기 위한 도구이며
    • CPU를 쓰는 일은 여전히 다른 메커니즘에 맡깁니다

    그래서 현대 파이썬 프로그램은 이렇게 구성됩니다.

    • I/O → async
    • CPU → executor / native code
    • 전체 흐름 → 이벤트 루프

    마무리

    executor를 이해하면, async에 대한 기대치도 함께 정리됩니다.

    async는 모든 것을 해결하지 않는다. 다만, 무엇을 어디에 맡겨야 하는지는 아주 명확하게 알려준다.

    이 경계를 이해하는 순간, async 코드는 더 이상 느리거나 답답한 기술이 아니라, 아주 정직한 설계로 보이기 시작합니다.

Designed by Tistory.