본문 바로가기
프로그래밍 언어

Python 비동기 Asyncio, coroutine 자세한 내용

by 내기록 2023. 3. 24.
반응형

 

비동기 프로그래밍이 필요한 이유

비동기 프로그래밍을 위해 Python 3.4 asyncio 라이브러리가 도입되었고, 3.5에서 async와 await 키워드가 도입되었습니다.

 

파이썬을 사용하다 보면 블로킹(blocking)을 경험했을 것입니다. 예를 들어 requests 라이브러리를 사용했을 때 requests.get(url)을 호출하면 프로그램이 멈칫하는 현상을 경험할 수 있는데 이는 블로킹으로 인한 결과입니다.

 

일회성 작업인 경우에는 괜찮을 수 있지만 동시에 10,000개의 URL을 호출한다면 requests를 적절히 호출하기란 쉽지 않습니다.

이는 대규모 병행성(동시성, concurrency)을 배우고 사용해야 하는 이유입니다.

 

Asyncio 소개와 스레딩과의 차이점

Asyncio의 목표는 대기를 필요로 하는 여러 개의 작업을 동시에 잘 수행하는 것입니다. 즉, 이 작업이 완료되길 기다리는 동안 다른 작업을 수행 할 수 있도록 해야 합니다.

 

I/O 위주 작업에 스레드 기반 병행 처리보다 비동기(asynchronous) 기반 병행 처리를 적용해야 하는 두 가지 이유가 있습니다.

- Asyncio는 스레드를 사용하는 선점형 멀티태스킹보다 안전한 대안이 될 수 있습니다.
  단순하지 않은 스레드 기반 애플리케이션에서 발생하는 오류나 경합 조건 등이 발생하지 않습니다.

- Asyncio를 통해 동시에 수천 개의 소켓 연결을 간단히 처리할 수 있습니다.

 

프로그래밍 모델 관점에서 보면, 스레딩은 여러 CPU와 공유 메모리 (스레드 간 효율적인 통신의 수단)를 사용하는 방식이기 때문에 계산 위주 작업을 가장 잘 수행할 수 있어 계산 위주 작업이 많은 분야에 적합합니다. 하지만 다른 문제들을 발생시킬 수 있습니다.

 

네트워크 프로그래밍은 스레딩을 필요로 하는 영역이 아닙니다. 네트워크 프로그래밍의 중요한 특징은 '어떤 일들이 일어나기를 기다리는' 작업들로 구성되어 있다는 점입니다. 따라서 여러 CPU에 작업들을 효율적으로 분배하기 위한 운영체제와의 연계 작업이 필요 없으며 공유 메모리 접근 시 발생할 수 있는 경합 조건 같은 리스크를 불러오는 선점형 멀티태스킹도 필요하지 않습니다.

 


목차

 

     


    Asyncio

    asyncio API들을 요약하면 아래와 같습니다.

     

    • asyncio 이벤트 루프 시작하기
    • async/await 함수 호출하기
    • 루프에서 실행할 태스크 작성하기
    • 여러 개의 태스크가 완료되길 기다리기
    • 모든 병행 태스크 종료 후 루프 종료하기

     

    예제)

    import asyncio, time
    
    async def main():
        print(f'{time.ctime()} Hello!')
        await asyncio.sleep(1.0)
        print(f'{time.ctime()} Goodbye!')
        
    asyncio.run(main())
    Thu Mar 23 22:35:33 2023 Hello!
    Thu Mar 23 22:35:34 2023 Goodbye!

    간단한 예제입니다. 비동기 함수는 async def 로 정의하고, asyncio.run()으로 비동기 함수를 실행시킵니다.

    아래 예제는 더 심화된 내용으로 자세히 살펴보겠습니다.

     

     

    예제2)

    이 예제가 더 복잡한 이유는 asyncio.run()에서 제공해주는 기능을 풀어서 적었기 때문입니다.

    import asyncio
    import time
    
    async def main():
        print(f'{time.ctime()} Hello!')
        await asyncio.sleep(1.0)
        print(f'{time.ctime()} Goodbye!')
        
    loop = asyncio.get_event_loop()         # 1
    task = loop.create_task(main())         # 2 코루틴 스케줄링
    loop.run_until_complete(task)           # 3
    loop.close()    # 4

    #1 : 코루틴을 실행하기 위해 루프 인스턴스를 얻습니다. 동일 스레드에서 호출하면 코드의 어디에서든 get_event_loop()의 호출 결과는 항상 똑같은 루프 인스턴스를 반환합니다. 하지만 async def 함수 내에서 호출하는 경우에는 'asyncio.get_running_loop()'를 호출해야 합니다.

     

    #2 : create_task() 호출 전까지 코루틴 함수는 실행되지 않습니다. create_task()를 호출해서 루프에 코루틴을 스케줄링 합니다.

    반환 받은 task 객체로 작업 상태를 모니터링 할 수 있고(실행중인지 혹은 완료됐는지), 코루틴 완료 후 코루틴이 반환한 값도 얻을 수 있습니다. 또한, task.cancle()로 작업을 취소할 수도 있습니다.

     

    #3 : run_until_complete() 호출을 통해 현재 스레드(보통 메인 스레드)를 블로킹합니다. 매개변수로 전달했던 코루틴이 완료될 때까지 루프를 실행합니다. 루프가 실행되는 동안 스케줄링된 다른 작업도 같이 실행됩니다. => 태스크가 모두 종료 상태가 될 때까지 대기

    * asyncio.run() 의 내부에 run_until_complete()가 포함되어 있습니다.

     

    #4 : loop.close() - 정지된 루프에 대해 최종적으로 호출해야 합니다. 이 함수는 루프의 모든 대기열을 비우고 익스큐터를 종료시킵니다. 정지된 루프는 다시 실행될 수 있으나, 닫힌 루프는 완전히 끝난 것입니다.

    * asyncio.run() 내부에서는 호출될 때마다 신규 이벤트 루프를 생성하고, 반환되기 전에 루프를 닫습니다.

     

    => 첫 번째 예제에서는 asyncio.run()을 호출하여 대부분의 절차를 내부적으로 포함하고 있어 별도 코드에 작성할 필요가 없었습니다. 하지만 이 절차를 잘 이해한다면 여러가지 문제 해결에 도움이 될 수 있습니다.

     

     

    예제3)

    간단하게 보면 좋은 예제입니다. create_task로 각각 코루틴을 스케줄링합니다.

    create_task로 태스크는 코루틴을 동시에 예약하는데 사용됩니다. 즉, 코루틴은 곧 실행되도록 자동으로 예약됩니다.

    await는 완료될 때까지 기다리면서 스케줄링된 다른 작업도 실행할 수 있도록 합니다.

    import asyncio
    import time
    
    async def say_after(delay, what):
        await asyncio.sleep(delay)
        print(what)
    
    async def main():
        task1 = asyncio.create_task(
            say_after(1,'Hello')
        )
    
        task2 = asyncio.create_task(
            say_after(2,'World')
        )
        print(f"started at {time.strftime('%X')}")
    
        await task1
        await task2
    
        print(f"finished at {time.strftime('%X')}")
    
    # execute the asyncio program
    asyncio.run(main())
    
    started at 22:30:24
    Hello
    World
    finished at 22:30:26

     

    Coroutine(코루틴)

    asyncio는 파이썬 3.4에서 처음으로 추가되었고, 코루틴을 사용하기 위한 새로운 async, def와 await는 파이썬 3.5에서 추가되었습니다. 그렇다면 3.4 버전에서는 어떻게 asyncio를 사용했을까요?
    그때는 제너레이터(generator)가 코루틴을 대신했습니다. 그래서 예전 코드를 보면 @asyncio.coroutine 으로 처리한 제너레이터 함수를 찾을 수 있고, 제너레이터 함수 내에는 yield from 구문이 들어 있습니다.
    3.5버전부터 도입된 새로운 문법 async def를 사용한 코루틴은 파이썬 언어에 내장된 코루틴 문법 내에 포함되었기 때문에 '네이티브 코루틴' 이라고 불립니다.

     

    "코루틴은 무엇인가? 코루틴은 완료되지 않을 채 일시 정지(suspend) 했던 함수를 재개할 수 있는 기능을 가진 객체 입니다."

     

    즉, 코루틴은 일시 중단했다가 다시 시작할 수 있는 함수로 코루틴은 종종 일반화된 서브루틴으로 정의됩니다.

    서브루틴은 한 지점에서 시작하여 다른 지점에서 끝나는 방식으로 실행될 수 있습니다. 반면 코루틴은 실행된 후 일시 중단되었다가 여러 번 다시 시작되어 최종적으로 종료될 수 있습니다.


    코루틴은 서브루틴의 보다 일반화된 형태입니다. 서브루틴은 한 지점에서 입력되고 다른 지점에서 종료되지만 코루틴은 다양한 지점에서 시작, 종료, 재개할 수 있습니다.

     

     

    새로운 키워드 await는 항상 매개변수 하나를 필요로 합니다. 허용되는 형은 awaitable 이라 불리며 다음 중 하나여야 합니다.

    - 코루틴(즉, async def 함수의 반환 값)

    - __await__() 라는 메서드를 구현한 모든 객체, 이 메서드는 반드시 이터레이터(iterator)를 반환해야 한다.
       일상에선 거의 사용X

     

     

    예제) 코루틴에 await 사용

    import asyncio
    
    
    async def f():
        await asyncio.sleep(0)
        return 123
    
    
    async def main():
        result = await f() #1
        return result
    
    asyncio.run(main())

    #1 : f()를 호출하면 코루틴을 반환합니다. 이는 f()에 대해 await 할 수 있다는 뜻입니다.

     

    파이썬 3.7 이전에는 태스크를 스케줄링 하기 위해 loop 인스턴스를 먼저 획득해야 했지만, get_running_loop()가 도입된 후로는 그것을 사용하는 asyncio.create_task()와 같은 asyncio 함수만으로 태스크를 생성할 수 있게 되었습니다.

    파이썬 3.7 이후의 비동기 태스크 생성 코드는 다음 예제와 같습니다.

     

    import asyncio
    
    
    async def f():
        # 태스크 생성
        for i in range():
            asyncio.create_task(<some other coro>)

     

    async with : 비동기 컨텍스트 관리자

    네트워크 연결과 같은 자원의 생명주기를 정의한 범위 내에서 관리하고자 할 때 비동기 컨텍스트 관리자를 사용하면 좋습니다. async with를 이해하기 위해서는 컨텍스트 관리자의 동작이 '메서드 호출'로 이뤄진다는 점을 깨달아야 합니다. 

     

    시작하기 전에, 컨텍스트 관리자란?

    컨텍스트 관리자(context manager)를 사용하면 원하는 시점에 정확하게 리소스를 할당하고 해제할 수 있습니다. 가장 널리 사용되는 예는 with 문입니다. 즉, connection open 과 같은 리소스 설정을 처리하고, 작업이 끝나면 자동으로 close 합니다. 

    with open('some_file', 'w') as opened_file:
        opened_file.write('Hola!')

     

     

    예제)

    from contextlib import asynccontextmanager
    
    @asynccontextmanager                   #1
    async def web_page(url):
        data = await download_webpage(url) #3
        yield data                         #4
        await update_stats(url)            #5
    
    
    async with web_page('google.com') as data:   #6
        process(data)

    #1 : 데코레이터를 사용하여 제너레이터 함수를 컨텍스트 관리자로 변환합니다.

    * 데코레이터(decorator) : 함수를 직접 수정하지 않고 함수에 기능을 확장할 때 사용

    * 제너레이터(generator) : iterator를 생성해주는 함수, 함수 내에 yield 키워드 사용

     

    #3 : await 키워드를 사용하여 코루틴에서 네트워크 호출이 완료될때까지 대기하는 동안, 이벤트 루프에서 다른 태스크를 처리할 수 있도록 합니다. 하지만 await 키워드를 아무데서나 단순하게 사용하면 안됩니다. download_webpage()에서 코루틴을 반환하도록 변경함으로써 await를 사용할 수 있도록 해야합니다. 함수를 직접 수정할 수 없는 경우에는 다른 방식을 적용해야 합니다. * 다른 방식은 아래에서 설명

     

    #4 : 컨텍스트 관리자의 본문에서 데이터를 사용할 수 있습니다. yield가 함수 내에 있으므로 제너레이터 함수가 되고, async def 키워드를 사용함으로써 비동기 제너레이터 함수가 됩니다. 이런 함수를 호출하면 비동기 제너레이터를 반환합니다.

     

    #5 : update_stats() 함수가 코루틴을 반환한다고 가정하고 await 키워드를 사용하면 I/O 위주 작업이 완료될 때까지 대기하며 이벤트 루프에서 컨텍스트 전환이 일어날 수 있도록 허용합니다.

     

    #6 : 컨텍스트 관리자 용법도 일반 with가 아닌 async with를 사용합니다.

     

     

     

    코루틴을 반환하지 않는 함수는 await을 사용할 수 없나요?

    import asyncio
    from contextlib import asynccontextmanager
    
    @asynccontextmanager
    async def web_page(url):
        loop = asyncio.get_event_loop()
        data = await loop.run_in_executor(                  #2
            None, download_webpage, url
        )
        yield data
        await loop.run_in_executor(None, update_stats, url)
        
    
    async with web_page('google.com') as data:
        process(Data)

    이전 예제에서 download_webpage와 update_stats 함수가 수정을 못하는 상황이라면 어떻게 될까요?

    코루틴을 반환하지 못해서 블로킹 상태로 머물러야 할까요?

     

    => 이벤트 기반 프로그래밍에서의 최악은 이벤트 루프가 이벤트를 처리하지 못하고 블로킹 상태에 머물도록 하는 것입니다.

    이 문제를 해결하기 위해 별도의 스레드에서 익스큐터로 블로킹을 호출하는 방식을 사용합니다. 익스큐터는 이벤트 루프의 속성으로 사용할 수 있습니다. 즉, run_in_executor() 함수로 쓰레드를 별도로 생성해서 블로킹 함수를 비동기로 사용할 수 있습니다.

     

     

    #2 : 익스큐터를 호출합니다. AbstractEventLoop.run_in_executor(executor, func, *args) 로 executor 인수를 None으로 전달하면 기본 익스큐터(ThreadPoolExecutor class)를 사용합니다.

     

     

     

    async for : 비동기 이터레이터

    for 루프의 비동기 버전으로, iterator가 특별 메서드(__메서드__)를 통해 구현되어 있다는 점을 이해하면 비동기 버전도 쉽게 이해할 수 있습니다.

     

    비동기 이터레이터에서 async for를 사용하려 할 경우 비동기 이터레이터에서 지켜야 하는 사항을 언어 명세(PEP 492)에서 정의하고 있습니다.

    1. def __aiter__() 구현 (async def가 아님)
    2. __aiter__()는 async def __anext__()를 구현한 객체를 반환해야 한다.
    3. __anext__()는 반복의 각 단계에 대한 값을 반환하고, 반복이 끝나면 StopAsyncIteration을 발생시켜야 한다.

     

    아래 예시는 Redis 데이터베이스에서 여러 개의 키를 하나씩 넣어서 데이터를 확인할 때, 각 데이터를 요청 시점에 가져오는 상황입니다.

    import asyncio
    import aioredis import create_redis
    
    
    async def main():
        redis = await create_redis(('localhost', 6379))
        keys = ['Americas', 'Africa', 'Europe', 'Asia']
    
        # 반복 중에 다음 데이터를 얻기 전까지 반복을 일시 정지할 수 있다.
        async for value in OneAtTime(redis, keys):
            # redis에서 얻은 데이터에 대해 I/O 위주 동작을 수행한다고 가정 
            await do_something_with(value) 
    
    class OneAtTime:
        # 클래스의 생성자(initizlizer)는 레디스 연결 인스턴스와 키값 목록을 저장
        def __init__(self, redis, keys):
            self.redis = redis
            self.keys = keys
        def __aiter__(self): #7
            self.ikeys = iter(self.keys)
            return self
        async def __anext__(self): #8
            try:
                k = next(self.ikeys) #9
            except StopIteration: #10
                raise StopAsyncIteration
    		
            # 데이터를 가져오는 작업이 완료될 때까지 await
            # 이벤트 루프에서 다른 동작 수행
            value = await self.redis.get(k)
            return value
    
    asyncio.run(main())

    __aiter__()

    • 반복을 수행하기 위한 준비 작업을 합니다. 키 목록으로 일반 이터레이터인 self.ikeys를 생성합니다.
    • OneAtTime 클래스는 async def  __anext__() 를 구현한 iterable한 객체이므로 return self를 수행합니다.

    __anext__()

    • async def로 선언
    • self.ikeys는 키 목록에 대한 일반적인 이터레이터로 next()를 호출하여 다음 키를 사용
    • self.ikeys를 모두 소모하여 StopIteration이 발생하면 StopAsyncIteration으로 전환
      -> 이것이 비동기 이터레이터 내에서 정지 신호를 발생시키는 방법

     

    이 예시로 알 수 있는 사실은, async for를 통해 for 루프의 편의성은 유지하면서 I/O를 반복 수행하는 동작은 비동기로 처리할 수 있다는 것입니다. 각각의 데이터 처리를 간단하게 유지할 수 있다면, 하나의 이벤트 루프만으로 엄청난 양의 데이터를 처리할 수 있습니다.

     

    비동기 제너레이터를 사용해서 더 간단하게 코드를 짤 수 있다.

    제너레이터는 이터레이터를 생성해주는 함수입니다. 제너레이터 함수로 생성한 객체는 이터레이터와 마찬가지로 next() 함수 호출 시 그 값을 차례대로 얻을 수 있습니다. 이때 제너레이터에서는 차례대로 결과를 반환하고자 return 대신 yield 키워드를 사용합니다.

    참고) generator와 yield

     

    비동기 제너레이터는 async def 함수로 내부에 yield 키워드를 포함합니다.

    • 코루틴과 제너레이터는 완전히 다른 개념
    • 비동기 제너레이터는 일반 제너레이터와 매우 유사하게 동작
    • 반복 수행 시, 비동기 제너레이터에 대해서는 async for를 사용

     

    직전에 비동기 이터레이터를 직접 구현했던 코드를 비동기 제너레이터를 사용해서 간단하게 바꿔보겠습니다. 

    OneAtTime 클래스가 one_at_a_time() 함수로 깔끔하게 변경된 것을 확인할 수 있습니다. 

    import asyncio
    import aioredis import create_redis
    
    
    async def main():
        redis = await create_redis(('localhost', 6379))
        keys = ['Americas', 'Africa', 'Europe', 'Asia']
    
        async for value in one_at_a_time(redis, keys):
            await do_something_with(value)
    
    # yield 키워드를 포함하여 비동기 제너레이터 함수가 됨
    async def one_at_a_time(redis, keys):
        for k in keys:
            value = await redis.get(k)
            # 일반적인 제너레이터처럼 호출자에게 값 전달
            yield value
    
    asyncio.run(main())

     

    비동기 제너레이터는 코드를 짧고 간단하게 만들어주기 때문에 asyncio 기반 코드에서 많이 사용되므로 익숙해지면 좋습니다.

     

     

    비동기 컴프리헨션(Comprehension)

    컴프리헨션이란 기존의 자료구조(list, dict, set)에 기반한 자료구조를 쉽게 만드는 파이썬 문법입니다.

    즉, 리스트 컴프리헨션은 리스트를 쉽고 짧게 만들 수 있는 파이썬 문법입니다.

    import asyncio
    
    # 비동기 제너레이터로 상한값이 주어지면 반복하며 두 배 값으로 이루어진 튜플을 전달(yield)
    async def doubler(n):
        for i in range(n):
            yield i, i*2                
            await asyncio.sleep(0.1)
    
    
    async def main():
        result = [x async for x in doubler(3)]         # 비동기 리스트 컴프헨션
        print(result)
        result = {x: y async for x, y in doubler(3)}   # 비동기 딕셔너리 컴프리헨션
        print(result)
        result = {x async for x in doubler(3)}         # 비동기 집합 컴프리헨션
        print(result)
    
    asyncio.run(main())
    [(0, 0), (1, 2), (2, 4)]
    {0: 0, 1: 2, 2: 4}
    {(2, 4), (1, 2), (0, 0)}

     

    비동기 컴프리헨션으로 만든 것은 await이 아닌 async for 입니다. 두 가지 별개의 기능을 같이 사용할 수도 있습니다. 아래 예제에서 자세히 살펴보겠습니다.

     

     

    예제)

    import asyncio
    
    
    async def f(x):
        await asyncio.sleep(0.1)
        return x + 100
    
    # 비동기 제너레이터 함수
    async def factory(n):        
        for x in range(n):
            await asyncio.sleep(0.1)
            # f는 코루틴 함수로 아직 코루틴은 아님
            yield f, x
    
    
    async def main():
        # 비동기 컴프리헨션으로 factor()을 호출해서 비동기 제너레이터를 반환받음
        # 비동기 제너레이터이므로 async for 사용
        results = [await f(x) async for f, x in factory(3)]
        print(f"results = {results}")
    
    asyncio.run(main())
    results = [100, 101, 102]

    비동기 제너레이터에서 생성된 값은 코루틴 함수 f와 x(int형)로 이루어진 튜플입니다. 코루틴 함수 f()를 호출하여 코루틴을 생성하고, 생성된 코루틴은 await를 사용해서 호출합니다.

    컴프리헨션 내에서 await의 사용 목적은 async for와 완전히 무관합니다. 완전히 다른 목적으로 사용했고 완전히 다른 객체에 작동하고 있다는 것을 이해해야 합니다.

     

     

    시작과 종료

    지금까지 시작은 단순하게 asyncio.run()만으로 가능했습니다. 시작에 비해 종료는 훨씬 복잡한데, async def main() 함수가 종료될 때 asyncio.run() 내에서 실행되는 동작은 다음과 같습니다.

     

    1. 아직 보류 중인 태스크 객체를 모두 수집
    2. 이 태스크들을 모두 취소(각 실행 중인코루틴 내에서 CancelledError가 발생하며, try/exception 처리 가능)
    3. 태스크를 모두 그룹 태스크로 수집
    4. 그룹 태스크에 대해 run_until_complete()를 사용하여 모든 태스크가 완료할 때까지 대기
      종료까지 대기한다는 의미는 대기 중인 태스크에 발생한 CencelledError exception이 처리될 때까지 대기한다는 의미

     

    즉, 종료 시에는 종료하지 않은 태스크들를 모아서, 모든 태스크에 cancle()을 호출하고, 모든 태스크가 종료할 때까지 대기한 다음 이벤트 루프를 종료합니다. asyncio.run()에서는 이 절차를 대신 실행해주지만, 복잡한 상황이 생겼을 때 처리할 수 있으려면 이 절차를 상세히 파악하고 있으면 좋습니다.

     

    gather() 사용 시 return_exceptions=True 사용하기

    기본 값은 gather(..., return_exceptions=False) 입니다. 대부분의 경우 이 기본값은 적합하지 않습니다.

    그 이유를 과정으로 살펴보겠습니다.

     

    1. run_until_complete()는 Future에 대해 작동합니다.
      종료 절차에서 gather()에서 반환하는 Future를 run_until_complete()에 전달

    2. run_until_complete()에 전달된 퓨처에서 예외가 발생한다면, 예외를 run_until_complete() 의 범위 밖으로 전달하고 이벤트 루프는 중지

    3. run_until_complete()를 그룹퓨처에서 사용하는 경우, 그룹 퓨처의 하위 퓨처에서 예외가 발생하고 그 예외 처리를 하지 못하면 그룹 퓨처 자체의 예외가 발생한 것이 됩니다. (CancelledError 포함)

    4. 일부 태스크만 CancelledError를 처리한다면 처리하지 않은 태스크들로 인해 모든 태스크가 종료되기 전에 이벤트 루프가 중지됩니다.

    5. 그룹 태스크의 일부 하위 태스크가 예외를 발생했을 때, 모든 하위 태스크가 종료된 뒤에 run_until_complete()가 반환되어야 함

    6. 그래서 gather(*, return_exceptions=True) 설정을 통해 그룹퓨처가 하위 태스크의 예외를 반환 값으로 처리하도록 하여 run_until_complete()가 중지되지 않도록 함
    pending = asyncio.all_tasks(loop=loop)
    asyncio.gather(*pending, return_exceptions=True)

     

    시그널

    KeyboardInterrupt는 SIGINT 시그널에 해당합니다. 그런데 네트워크 서비스에서 프로세스 종료와 관련된 일반적인 시그널은 SIGTERM으로, 유닉스 셸의 kill 명령어 실행 시 발생하는 기본 시그널입니다.

    import asyncio
    from signal import SIGINT, SIGTERM
    
    
    async def main():
        loop = asyncio.get_running_loop()
        for sig in (SIGTERM, SIGINT):
            loop.add_signal_handler(sig, handler, sig)
    
        try:
            while True:
                print('<Your app is running>')
                await asyncio.sleep(1)
        except asyncio.CancelledError:
            for i in range(3):
                print('Your app is shutting down...')
                await asyncio.sleep(1)
    
    
    def handler(sig): #시그널 처리기를 정의한 함수 handler
        loop = asyncio.get_running_loop()
        for task in asyncio.all_tasks(loop=loop):        #2
            task.cancel()
        print(f'Got signal: {sig!s}, shutting down.')
        loop.remove_signal_handler(SIGTERM)              #3
        loop.add_signal_handler(SIGINT, lambda: None)    #4
    
    
    if __name__ == '__main__':
        asyncio.run(main())

     

    #2 : 이벤트 루프를 중지하게 되면 main()을 처리하는 태스크가 비정상적으로 중지되기 때문에 시그널 처리기 내부에서 태스크에 취소를 요청(cancle 메서드 호출)합니다. main() 태스크가 정상적으로 완료되면 asyncio.run() 내의 정리 작업이 진행됩니다.

    #3 : 종료 절차 중에 SIGINT나 SIGTERM 시그널이 또 들어와도 처리기를 실행하지 않도록 합니다. 만약 또 처리기를 실행하게 되면 태스크 취소 요청이 꼬일 수 있습니다. 따라서 이벤트 루프에서 SIGTERM 시그널 처리기를 제거합니다.

    #4 : 단순히 SIGINT 처리기를 제거하면 KeyboardInterrupt가 다시 SIGINT 처리기가 되기 때문에 빈 lambda 함수를 처리기로 설정하여 KeyboardInterrupt 즉, SIGINT(Ctrl-c)가 다시 들어와도 효과가 없도록 합니다.

     

     

     

    References

    파이썬 비동기 라이브러리 Asyncio=> asyncio 및 전반적인 내용에 대해 자세히 해주는 책

    반응형

    댓글