Asyncio

Python의 asyncio비동기 I/O 프로그래밍을 지원하는 표준 라이브러리로, async/await 구문을 활용해 동시성 코드를 작성할 수 있게 해 준다. 특히 I/O 바운드 작업(네트워크 통신, 파일 입출력 등)에서 성능을 극대화할 수 있으며, Python의 GIL(Global Interpreter Lock) 제약을 우회하는 싱글 스레드 기반 동시성 구현이 가능하다.

Python 3.4부터 도입된 asyncio는 웹 서버, 데이터 수집기, 실시간 애플리케이션 등 I/O 집약적인 작업에서 혁신적인 성능 향상을 제공한다. 그러나 동기식 코드 베이스와의 통합 시 주의가 필요하며, 비동기 지원 라이브러리(aiohttp, asyncpg 등)와의 조합이 효과적이다.

핵심 개념

이벤트 루프(Event Loop)

모든 비동기 작업의 스케줄링과 실행을 관리하는 중심 메커니즘이다.
asyncio.run()을 호출하면 내부적으로 이벤트 루프가 생성되고 실행된다.

기본 작동 원리
  1. 이벤트 루프 생성

    1
    2
    3
    4
    5
    6
    7
    
    import asyncio
    
    async def main():
        print("Hello 이벤트 루프!")
    
    # 이벤트 루프 자동 생성 및 실행
    asyncio.run(main())  # [6][12]
    
    • asyncio.run()이 새 이벤트 루프 생성 → 작업 실행 → 종료를 자동 관리
    • 메인 스레드에서 기본적으로 사용되며, 별도 설정 없이 활용 가능
  2. 작업 스케줄링 프로세스

    1. 코루틴 등록: create_task()로 코루틴을 태스크로 변환
    2. 실행 큐 관리: 준비된 태스크를 순차적으로 실행
    3. I/O 대기 처리: await 발생 시 현재 작업 일시 중지, 다음 태스크로 전환
    4. 이벤트 모니터링: I/O 완료 신호 감지 시 해당 태스크 재개
  3. 실행 흐름 예시

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    async def task_a():
        print("A 시작")
        await asyncio.sleep(2)
        print("A 완료")
    
    async def task_b():
        print("B 시작")
        await asyncio.sleep(1)
        print("B 완료")
    
    asyncio.run(asyncio.gather(task_a(), task_b()))
    

    출력 결과:

    1
    2
    3
    4
    
    A 시작
    B 시작  # I/O 대기 시간 활용
    B 완료  # 1초 후
    A 완료  # 2초 후
    
    • await에서 실행 권한 반환 → 이벤트 루프가 다른 작업 스케줄링
주요 동작 메커니즘
  1. 작업 상태 관리
상태설명전환 조건
Pending실행 대기 중태스크 생성 직후
Running현재 실행 중이벤트 루프가 선택
Done정상 완료return 실행
Cancelled취소됨task.cancel() 호출
Failed예외 발생처리되지 않은 에러[4][14]
  1. 스케줄링 전략

    1. 준비 상태 검사: selector 모듈로 I/O 이벤트 모니터링
    2. 우선순위 큐: 콜백이 등록된 순서대로 처리(call_soon())
    3. 시간 기반 스케줄링: loop.call_later(delay, callback)
  2. 실행 모드 비교

    1
    2
    3
    4
    5
    
    # 단일 작업 실행
    loop.run_until_complete(task)  # [1][4]
    
    # 무한 실행(서버 등)
    loop.run_forever()  # [1][4]
    
    • run_until_complete: 지정 작업 완료 시 루프 종료
    • run_forever: 명시적 loop.stop() 호출 필요
고급 기능
  1. 다중 이벤트 루프

    1
    2
    3
    
    # 새 루프 생성 및 설정
    new_loop = asyncio.new_event_loop()
    asyncio.set_event_loop(new_loop)
    
    • 스레드별 독립적 루프 운영 가능
    • 주의: 동일 스레드에선 항상 get_event_loop() 사용 권장
  2. 에러 핸들링

    1
    2
    3
    4
    5
    
    def handle_exception(loop, context):
        print(f"에러 발생: {context['exception']}")
    
    loop = asyncio.get_event_loop()
    loop.set_exception_handler(handle_exception)
    
  3. 성능 모니터링

    1
    2
    
    # 디버그 모드 활성화
    loop.set_debug(True)
    
    • 태스크 생성/소멸 추적
    • 느린 콜백 경고 출력
실제 적용 사례 (FastAPI)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from fastapi import FastAPI
import asyncio

app = FastAPI()

async def db_query():
    await asyncio.sleep(0.5)
    return "데이터"

@app.get("/")
async def read_data():
    result = await db_query()  # I/O 대기 시 다른 요청 처리
    return {"data": result}
  1. 요청 수신 → 코루틴 생성
  2. await db_query()에서 I/O 대기
  3. 이벤트 루프가 다른 요청 처리로 전환
  4. DB 응답 도착 시 원래 코루틴 재개
주의사항
  • 동기 코드 혼용 금지: time.sleep() 대신 asyncio.sleep() 사용
  • CPU 바운드 작업 회피: loop.run_in_executor()로 별도 스레드 처리
  • 루프 중복 실행 방지: RuntimeError 발생 가능

이벤트 루프는 Python의 싱글 스레드 병행성을 실현하는 핵심이다.
I/O 대기 시간을 최대한 활용하여 웹 서버, 실시간 스트리밍, 대규모 연결 관리 등에 효과적이다.

코루틴(Coroutine)

async def로 정의되며 await 키워드로 실행을 일시 중지/재개할 수 있는 함수이다.
예시:

1
2
3
async def fetch_data():
  await asyncio.sleep(1)
  return "데이터 수신 완료"

태스크(Task)

코루틴을 이벤트 루프에서 실행 가능한 단위로 래핑한다.
asyncio.create_task()로 생성하며, 여러 태스크를 동시 실행할 수 있다:

1
2
3
4
5
async def main():
  task1 = asyncio.create_task(fetch_data())
  task2 = asyncio.create_task(process_data())
  await task1
  await task2

주요 기능 및 활용 사례

  1. 기본 사용 패턴

    1
    2
    3
    4
    5
    6
    7
    8
    
    import asyncio
    
    async def main():
        print("시작")
        await asyncio.sleep(1)
        print("1초 후 종료")
    
    asyncio.run(main())
    
    • asyncio.run()이 이벤트 루프를 시작하고 최상위 코루틴을 실행한다.
  2. 병렬 작업 처리

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    async def download(url):
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                return await response.text()
    
    async def main():
        urls = ["http://example.com", "http://example.org"]
        tasks = [download(url) for url in urls]
        results = await asyncio.gather(*tasks)
    
    • asyncio.gather()로 여러 코루틴을 동시 실행하고 결과를 수집한다.
  3. 타임아웃 제어

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    async def long_operation():
        await asyncio.sleep(10)
        return "완료"
    
    async def main():
        try:
            result = await asyncio.wait_for(long_operation(), timeout=3)
        except asyncio.TimeoutError:
            print("작업 시간 초과")
    
    • wait_for()로 작업 제한 시간을 설정할 수 있다.

장단점 비교

장점단점
I/O 작업 대기 시간 활용 효율화CPU 바운드 작업에는 적합하지 않음
싱글 스레드에서 수만 개의 연결 처리 가능동기식 코드와의 통합 복잡성
기존 스레드/프로세스 방식 대비 리소스 절약[디버깅과 예외 추적 어려움

고급 활용 전략

  1. 비동기 컨텍스트 관리자
    __aenter____aexit__ 메서드 구현:

    1
    2
    3
    4
    5
    6
    
    class AsyncDB:
        async def __aenter__(self):
            await self.connect()
            return self
        async def __aexit__(self, *args):
            await self.close()
    
  2. 작업 취소 메커니즘

    1
    2
    3
    4
    5
    6
    7
    
    task = asyncio.create_task(long_running_job())
    await asyncio.sleep(0.5)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("작업 취소됨")
    
  3. 이벤트 루프 직접 제어

1
2
3
4
5
loop = asyncio.new_event_loop()
try:
    loop.run_until_complete(main())
finally:
    loop.close()

Python의 asyncioGIL(Global Interpreter Lock)

Python의 asyncio와 **GIL(Global Interpreter Lock)**은 동시성 처리 메커니즘에서 상호보완적인 역할을 한다. GIL은 CPython에서 멀티스레딩의 병렬 실행을 제한하지만, asyncio싱글 스레드 내에서 비동기 I/O 작업을 최적화함으로써 이 제약을 우회한다.

asyncio는 GIL의 제약 하에서도 I/O 병목을 해결하는 최적의 도구이지만, CPU 집약적 작업에서는 여전히 GIL의 영향을 받는 Python의 구조적 특성을 이해해야 한다. 이 조합은 웹 서버, 데이터 수집기 등 고성능 I/O 애플리케이션 구축에 효과적이다.

핵심 관계 분석

  1. GIL의 동작 특성과 Asyncio의 전략

    • GIL의 제약: 한 번에 하나의 스레드만 Python 바이트코드 실행 가능. CPU 바운드 작업에서 멀티스레딩의 병렬성 저하.
    • asyncio의 접근:
      • 이벤트 루프 기반으로 단일 스레드 내에서 코루틴 전환을 통해 동시성 구현.
      • I/O 대기 시간에 다른 코루틴을 실행해 CPU 유휴 시간 최소화.
    1
    2
    3
    4
    
    # 비동기 I/O 작업 예시 (출처 3)
    async def fetch_data():
        await asyncio.sleep(1)  # GIL 해제 후 다른 코루틴 실행
        return "데이터"
    
  2. 성능 비교: 스레딩 vs. Asyncio

특성스레딩asyncio
실행 단위OS 스레드코루틴
GIL 영향직접적 영향 (멀티코어 X)간접적 영향 없음[5][15]
적합 작업I/O + 제한적 CPU순수 I/O 집약적
컨텍스트 전환OS 스케줄러이벤트 루프 자체 관리
  1. GIL 회피 메커니즘
    • I/O 작업 시 GIL 자동 해제: asyncio.sleep(), 파일/네트워크 I/O에서 다른 코루틴 실행 가능.
    • 코루틴 스케줄링: 이벤트 루프가 실행 큐 관리로 명시적 스레드 전환 불필요.

실제 동작 시나리오

  1. 웹 서버가 100개의 동시 요청 처리
  2. 각 요청에서 await database.query() 실행
  3. I/O 대기 시간에 이벤트 루프가 다음 코루틴 실행
  4. 단일 스레드로 모든 요청 처리

주의사항 및 한계

  • CPU 바운드 작업: 행렬 연산 등 CPU 집약적 작업에서는 multiprocessing 모듈 필요.
  • 동기 코드 혼합: time.sleep() 대신 asyncio.sleep() 사용 필요.
  • GIL 완전 회피 아님: C 확장 모듈 사용 시 별도 GIL 관리 필요.

참고 및 출처