Python 강좌 / 스레드·프로세스·asyncio의 차이와 선택 기준
스레드·프로세스·asyncio는 여러 작업을 진행하는 서로 다른 방법입니다. 작업이 CPU 계산에 시간을 쓰는지, 외부 응답을 기다리는지와 사용하는 라이브러리의 지원 방식에 따라 선택해야 합니다.
동시 진행과 병렬 실행 구분하기
| 방법 | 특징 | 확인할 비용 |
|---|---|---|
| 스레드 | 같은 프로세스의 메모리를 공유 | 공유 상태 동기화·라이브러리 동작 |
| 프로세스 | 별도 메모리 공간에서 실행 | 시작·직렬화·데이터 전달 비용 |
| asyncio | 협력적으로 대기 지점을 넘기며 진행 | 비동기 라이브러리·이벤트 루프 차단 |
일반적인 GIL 활성 CPython에서는 순수 Python CPU 계산이 스레드를 늘린다고 곧바로 병렬 가속되는 것은 아닙니다. GIL 해제 확장이나 free-threaded 빌드 등 실행 환경에 따라 조건이 달라지므로 구현·빌드·라이브러리를 함께 확인합니다.
관련 동작과 지원 조건은 Python: concurrent.futures에서 확인할 수 있습니다.
대기 작업의 경계와 수집 실패은 HTTP와 API 기초: 요청·응답·JSON·오류 처리 및 종합 실습: API 데이터 수집하고 중복 없이 저장하기에서 사용하는 규칙과 연결됩니다.
전체 예제와 실행 결과
별도의 실습 폴더에서 아래 파일을 UTF-8로 저장합니다. 가상환경을 선택한 터미널에서 python concurrency.py 명령으로 실행합니다. macOS·Ubuntu에서 가상환경을 사용하지 않는 경우 명령 이름을 python3으로 바꿉니다.
concurrency.py 파일의 전체 내용입니다.
import asyncio
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def square(value):
return value * value
async def async_square(value):
await asyncio.sleep(0.01)
return square(value)
async def gather_values():
return await asyncio.gather(*(async_square(x) for x in [1, 2, 3]))
def main():
with ThreadPoolExecutor(max_workers=2) as pool:
print(list(pool.map(square, [1, 2, 3])))
with ProcessPoolExecutor(max_workers=2) as pool:
print(list(pool.map(square, [1, 2, 3])))
print(asyncio.run(gather_values()))
if __name__ == "__main__":
main()
예상 결과는 다음과 같습니다.
[1, 4, 9]
[1, 4, 9]
[1, 4, 9]
진입점·대기·실행 순서
Windows의 프로세스 생성 등에서는 새 프로세스가 모듈을 가져오므로 main 보호가 중요합니다. 작업 함수 square를 모듈 최상위에 정의해 프로세스가 전달받아 실행할 수 있게 했습니다. 노트북이나 대화형 환경에서는 프로세스 실행 조건이 다를 수 있으므로 .py 파일로 실행합니다.
async 함수도 await 없이 CPU 계산이나 동기 네트워크 호출을 오래 실행하면 이벤트 루프를 막을 수 있습니다. asyncio.sleep은 대기 시간을 다른 작업에 양보하는 예제이며 실제 HTTP 요청을 수행하는 것은 아닙니다.
GIL(Global Interpreter Lock)은 일반적인 GIL 사용 CPython에서 한 시점에 Python 바이트코드를 실행하는 스레드를 제한하는 잠금입니다. I/O 대기는 ThreadPoolExecutor, 순수 Python CPU 작업은 ProcessPoolExecutor, 비동기 대응 I/O는 asyncio를 우선 검토하되 실제 부하로 측정해야 합니다. free-threaded 빌드나 GIL을 해제하는 확장 모듈에는 다른 조건이 적용됩니다. 기존 제곱 예제는 API 사용법이며 속도 우열의 증거가 아닙니다. 작업 수·대기 시간을 제한하고 Future.result의 예외를 확인합니다. 아래 취소 실험은 finally 정리 후 취소를 호출자가 관찰하는 최소 예제입니다.
아래 전체 코드를 cancel.py로 저장하고 같은 실습 폴더에서 python cancel.py로 실행합니다.
import asyncio
async def worker():
try:
await asyncio.sleep(60)
finally:
print("worker cleanup")
async def main():
task = asyncio.create_task(worker())
await asyncio.sleep(0)
task.cancel()
try:
await task
except asyncio.CancelledError:
print("cancel confirmed")
if __name__ == "__main__":
asyncio.run(main())
예상 결과:
worker cleanup
cancel confirmed
다음은 제출한 작업의 실패를 result에서 확인하는 예입니다. max_workers는 동시에 실행하는 작업 수를 제한하며 대기열 전체의 크기 제한은 아닙니다. timeout은 기다리는 시간을 제한할 뿐 이미 실행 중인 작업을 강제로 중단하지 않습니다. 많은 입력은 배치로 나누어 제출하고, asyncio에서는 동시 I/O에 Semaphore 같은 제한을 별도로 적용합니다.
아래 전체 코드를 future_failure.py로 저장하고 같은 실습 폴더에서 python future_failure.py로 실행합니다.
from concurrent.futures import ThreadPoolExecutor
def calculate(value):
if value < 0:
raise ValueError("negative input")
return value * value
with ThreadPoolExecutor(max_workers=2) as pool:
futures = [pool.submit(calculate, value) for value in [2, -1]]
for future in futures:
try:
print(future.result(timeout=5))
except ValueError as error:
print("failed:", error)
예상 결과:
4
failed: negative input
이 동작과 적용 조건의 공식 설명: Python: asyncio task cancellation · Python: concurrent.futures
성능과 오류를 따로 확인하기
이 작은 제곱 예제는 API 차이를 보여 주며 성능 비교 자료가 아닙니다. 작은 작업은 프로세스 생성 비용 때문에 오히려 느릴 수 있습니다. 실제 데이터·환경에서 시간을 측정하고 작업 수·동시 실행 수·메모리를 함께 기록합니다.
map은 입력 순서에 맞춘 결과를 제공하지만 작업 완료 순서는 다를 수 있습니다. 공유 리스트를 여러 스레드에서 바꾸거나 무제한 작업을 생성하지 말고 동시성 제한·오류 전파·취소·종료 처리를 설계합니다. asyncio.run은 이미 실행 중인 이벤트 루프 안에서 다시 호출할 수 없다는 점도 확인하세요.




