asyncio

Nam Eun-Ji·2020년 11월 16일
1

python-비동기

목록 보기
4/4
post-custom-banner

파이썬 3.5이상에서는 Coroutine을 좀 더 계획적이고 더 나은 환경을 제공한다. 파이썬에 내장된 asyncio 패키지를 사용하는 것이다.
asyncio는 async/await 구문을 사용하여 동시성 코드를 작성하는 라이브러리이다.



문법

async, await

동기 방식
def 키워드로 선언하는 모든 함수는 기본적으로 동기 방식으로 동작한다.

def do_sync():
	pass

비동기 방식

  • 기존 def 키워드 앞에 async 키워드를 붙여주면 해당 함수는 비동기 처리되며, 이러한 비동기 함수를 파이썬에서는 코루틴이라고 부른다.
async def do_async():
	pass
  • 이러한 비동기 함수를 호출하면 코루틴 객체가 리턴된다.
do_async() # <coroutine object do_async at 0x1038de710>
  • 따라서 비동기 함수는 일반적으로 async로 선언된 다른 비동기 함수 내에서 await 키워드를 붙여서 호출해야 합니다.
async def main_async():
    await do_async()
  • async로 선언되지 않은 일반 동기 함수 내에서 비동기 함수를 호출하려면 asyncio 라이브러리의 이벤트 루프를 이용해야한다.
loop = asyncio.get_event_loop()
loop.run_until_complete(main_async())
loop.close()
  • 파이썬 3.7 이상에서는 다음과 같이 한 줄로 간단히 비동기 함수를 호출할 수도 있다.
asyncio.run(main_async())


awaitable

객체가 await 표현식에서 사용될 수 있을 때 어웨이터블 객체라고 말한다.
어웨이터블 객체에는 코루틴, 태스크, 퓨처 세 가지 주요 유형이 있다.

coroutine

import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    print(f"started at {time.strftime('%X')}")

    await say_after(1, 'hello')
    await say_after(2, 'world')
    await say_after(2, 'python')

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

# started at 15:13:45
# hello
# world
# python
# finished at 15:13:50

task

  • 태스크는 코루틴을 동시에 예약하는 데 사용된다.
  • 코루틴이 asyncio.create_task()와 같은 함수를 사용하여 태스크로 싸일 때 코루틴은 곧 실행되도록 자동으로 예약된다.
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'))
    task3 = asyncio.create_task(say_after(2, 'python'))


    print(f"started at {time.strftime('%X')}")

    await task1
    await task2
    await task3

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

# started at 15:12:26
# hello
# world
# python
# finished at 15:12:28

위 코루틴 코드와 태스크를 비교해보자면 코루틴은 총 5초가 걸리고, 태스크는 2초가 걸리는 것을 확인할 수 있다. 그 이유는 async def로 선언 된 함수를 호출하면, 코드가 실행 되지 않고 코루틴 객체를 리턴하기만 할 뿐이기 때문이다. 그리고 create_task는 이 반환된 객체를 가지고 비동기 작업 객체인 태스크를 만들고, 실행한다.

async def안에 작성한 코드가 실행되는 시점은 코루틴에서는 await say_after(1, "hello")이고, say_after(1, "hello") 호출로 코루틴 객체가 생성되고, await에 의해 함수 내 작성한 코드가 동기적으로 실행되면서 끝날 때까지 대기한다. 때문에 먼저 1초, 2초, 2초 순차적으로 기다린다.

반면에 태스크는 task1 = asyncio.create_task(say_after(1, 'hello')) 이 부분에서 이미 코드를 비동기로 실행했다. 그리고 await task 는 그저 이미 실행한 코드를 대기할 뿐이다. 따라서 task1, task2, task3의 await 순서를 바꿔도 같은 결과를 얻는다.


동시에 여러 태스크(코루틴) 실행하기

awaitable asyncio.gather(*aws, loop=None, return_exceptions=False)

인자로 넘어온 모든 어웨이터블이 성공적으로 완료되면, 결과는 반환된 값들이 합쳐진 리스트를 반환한다. (결괏값의 순서는 aws에 있는 어웨이터블의 순서와 일치)

import asyncio
import time

async def coroutine1():
    await asyncio.sleep(1)
    print("Hello coroutine1!")
    return "coroutine1"
 
async def coroutine2():
    await asyncio.sleep(2)
    print("Hello coroutine2!")
    return "coroutine2"
 
async def coroutine3():
    await asyncio.sleep(1)
    print("Hello coroutine3!")
    return "coroutine3"
 
async def main():
    print(f"started at {time.strftime('%X')}")

    # 코루틴 실행 결과(리턴값)가 coroutine_list 변수에 list로 담긴다.
    coroutine_list = await asyncio.gather(coroutine1(), coroutine2(), coroutine3())
    print(coroutine_list)

    print(f"finished at {time.strftime('%X')}")
 
if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

# started at 15:50:19
# Hello coroutine1!
# Hello coroutine3!
# Hello coroutine2!
# ['coroutine1', 'coroutine2', 'coroutine3']
# finished at 15:50:21

future

  • 비동기 연산의 최종 결과를 나타내는 특별한 저수준 어웨이터블 객체
  • Future object가 await될 경우, 다른 곳에서 resolve 되기 전 까지 해당 coroutine이 기다리게 된다.
  • 콜백 기반 코드를 async/await와 함께 사용하려면 asyncio의 Future 객체가 필요하다.
  • 일반적으로 응용 프로그램 수준 코드에서 Future 객체를 만들 필요는 없습니다.
  • 때때로 라이브러리와 일부 asyncio API에 의해 노출되는 Future 객체를 기다릴 수 있다.


event loop

이벤트 루프는 모든 asyncio 응용 프로그램의 핵심이다. 이벤트 루프는 비동기 태스크 및 콜백을 실행하고 네트워크 IO 연산을 수행하며 자식 프로세스를 실행한다.

이벤트 루프는 작업들을 루프(반복문)를 돌면서 하나씩 실행시키는 역할을 한다. 이때, 만약 실행된 작업이 특정한 데이터를 요청하고 응답을 기다려야 한다면, 이 작업은 다시 이벤트 루프에 통제권을 넘겨준다. 통제권을 받은 이벤트 루프는 다음 작업을 실행하게 된다. 그리고 응답을 받은 순서대로 멈췄던 부분부터 다시 통제권을 가지고 작업을 마무리한다.

이벤트 루프 얻기 메서드
이벤트 루프 메서드



대표적인 메소드

asyncio.run(coro, *, debug=False)

전달 된 코루틴을 실행하고, asyncio의 event loop를 관리한다. 같은 thread에서 다른 asyncio event loop가 실행중일 경우에는 실행할 수 없다. Python 3.7에서 새로 추가되었다.


asyncio.create_task(coro)

전달 된 코루틴을 wrapping하여 Task object를 만들고 schedule 한다. Python 3.7에서 새로 추가되었고, 이전 Python에서는 asyncio.ensure_future()를 사용한다.


asyncio.sleep(delay, result=None, loop=None)

delay초 동안 대기한다. result가 주어진 경우, sleep이 완료된 후 해당 값을 caller에게 리턴한다. sleep() 함수는 현재 task를 suspend 시키므로, 이 시점에 다른 task가 실행될 수 있다.


asyncio.gather(*aws, loop=None, return_exceptions=None)

주어진 awaitable objects들을 concurrent하게 실행한다. 주어진 awaitable object 중 코루틴이 있으면, 자동으로 Task로 schedule된다. 모든 awaitable들이 정상적으로 끝나면, 각 awaitable의 return value의 list가 return 된다. 순서는 aws의 순서와 동일하다.


asyncio.wait_for(aw, timeout, *, loop=None)

주어진 awaitable을 timeout초 동안 기다린다. aw가 코루틴 경우, 자동으로 Task로 schedule 된다. 해당 시간이 지나면, asyncio.TimeoutError가 발생한다.


자세한 내용은 아래 참고.

저수준 API

  • 네트워킹, 자식 프로세스 실행, OS 시그널 처리 등의 비동기 API를 제공하는 이벤트 루프를 만들고 관리한다.
  • 트랜스포트를 사용하여 효율적인 프로토콜을 구현한다.
  • 콜백 기반 라이브러리와 async/await 구문을 사용한 코드 간에 다리를 놓는다.
  • https://python.flowdas.com/library/asyncio-llapi-index.html

고수준 API

  • 파이썬 코루틴들을 동시에 실행하고 실행을 완전히 제어할 수 있다.
  • 네트워크 IO와 IPC를 수행한다.
  • 자식 프로세스를 제어한다.
  • 큐를 통해 작업을 분산한다.
  • 동시성 코드를 동기화한다.
  • https://python.flowdas.com/library/asyncio-api-index.html



참고
https://kdw9502.tistory.com/6
https://sjquant.tistory.com/15

profile
한 줄 소개가 자연스러워지는 그날까지
post-custom-banner

0개의 댓글