본문 바로가기
Language/Python

[Python] 비동기 프로그래밍 | LIM

by forestlim 2022. 7. 9.
728x90
반응형

파이썬에서 비동기 프로그래밍을 하기 위해서는 이벤트 루프코루틴 을 알고 있어야 한다.

 

📌 이벤트 루프(Event Loop)

이벤트 루프는 작업들을 루프를 돌면서 하나씩 실행시키는 역할을 한다. Callback Event Queue에서 하나씩 꺼내서 동작시키는 Loop를 말한다. 즉 이 이벤트 루프를 이용해서 비동기 방식으로 동시성을 지원하는 것이다. 만약, 실행된 작업이 특정한 데이터를 요청하고 응답을 기다려야 한다면, 이 작업은 다시 이벤트 루프에 통제권을 넘겨준다. 통제권을 받은 이벤트 루프는 다음 작업을 실행하게 된다. 그리고 응답을 받은 순서대로 멈췄던 부분분터 다시 통제권을 가지고 작업을 마무리 한다. 

 

📌 코루틴(Cooperative Routine)

위에 이러한 작업이 파이썬에서는 코루틴으로 이루어져 있는 것이다. 코루틴은 응답이 지연되는 부분에서 이벤트 루프에 통제권을 줄 수 있으며, 응답이 완료되었을 때 멈추었던 부분부터 기존의 상태를 유지한 채 남은 작업을 완료할 수 있는 함수를 의미한다. 즉, native coroutine 에서는 

async def 로 선언된 함수를 의미한다. python 3.5 부터 native coroutine 지원을 위한 async, await 키워드가 추가되었다. 

 

 

코루틴의 동작 과정(참고: https://dojang.io/mod/page/view.php?id=2418)

 

 

이처럼 코루틴은 함수가 종료되지 않은 상태에서 메인 루틴의 코드를 실행한 뒤 다시 돌아와서 코루틴의 코드를 실행한다. 따라서 코루틴이 종료되지 않았으므로 코루틴의 내용도 계속 유지된다. 즉, 코루틴은 각 루틴이 종속적인 관계가 아닌 대등한 관계라고 보면 된다.

 

📌 asyncio.run()

파이썬에서 비동기를 사용하기 위해서는 asyncio 모듈을 사용한다. 함수앞에 async를 붙이면 코루틴을 만들 수 있다. asyncio.run()은 코루틴을 동기적으로, 즉 코루틴이 끝날 때 까지 실행한다. 비동기 함수 내부에서는 사용하지 않는다. asyncio 프로그램의 메인 진입점으로 사용해야 하고, 한번만 호출되어야 한다. asyncio.run()은 파이썬 3.8부터 사용가능하다. 

 

주의할 부분은 코루틴 내에서는 time.sleep()을 사용할 수 없다는 것이다. 동기적으로 작동하는 코드이기 때문에 코루틴으로 구현된 asyncio.sleep() 을 써주어야 한다. 코루틴이 아닌 함수를 비동기에서 다루는 법은 다음번에 작성하도록 하겠다. 

import time
import asyncio


async def access_site(url):
    await asyncio.sleep(2)
    print('access site:', url)


async def main():
    await access_site('url_1')
    await access_site('url_2')
    await access_site('url_3')


start = time.time()
asyncio.run(main())
end = time.time()
print('총 걸린시간:', end - start)
access site: url_1
access site: url_2
access site: url_3
총 걸린시간: 6.005074977874756

 

분명히 asyncio.run()을 통해 비동기를 적용했음에도 불구하고 동기적으로 실행되어 6초가 걸린 걸 볼 수 있다. 그 이유는 await 키워드(작업이 종료될 때까지 기다림)가 access_site() 함수 앞에 모두 붙어 있기 때문이다. 따라서 함수가 종료될때까지 다른 함수가 실행되지 않는다. 

 

따라서 이와 같은 상황을 방지하기 위해서는 이벤트 루프에 모든 작업들을 한 번에 등록해야 한다. 두가지 방법이 있다. 

 

1) asyncio.create_task()
2) asyncio.gather()

 

🧐 asyncio.create_task()는 각각 awaitable 함수를 이벤트 루프에 등록하는 것이다. 

import time
import asyncio


async def access_site(url):
    await asyncio.sleep(2)
    print('access site:', url)

async def main():
    task1 = asyncio.create_task(access_site("url_1"))
    task2 = asyncio.create_task(access_site("url_2"))
    task3 = asyncio.create_task(access_site("url_3"))

    await task1
    await task2
    await task3

start = time.time()
asyncio.run(main())
end = time.time()
print('총 걸린시간:', end - start)
access site: url_1
access site: url_2
access site: url_3
총 걸린시간: 2.002357006072998

비동기가 적용되어서 총 걸린시간은 2초, 원하던 결과가 나왔다!

async def로 선언된 함수를 호출하면, 코드가 실행되지 않고 코루틴 객체를 리턴하기만 할 뿐이기 때문에 create_task를 통해서 이 반환된 객체를 가지고 비동기 작업 객체인 태스크를 만들고 실행해야하는 것이다. 

 

🧐 asyncio.gather()는 등록해야 하는 함수가 많을 때 유용하다. 위에 asyncio.create_task()가 각각 awaitable 함수를 등록한 것과는 다르다.

import time
import asyncio


async def access_site(url):
    await asyncio.sleep(2)
    print('access site:', url)

async def main():
    await asyncio.gather(*[access_site(url) for url in ["url_1", "url_2", "url_3"]])

start = time.time()
asyncio.run(main())
end = time.time()
print('총 걸린시간:', end - start)
access site: url_1
access site: url_2
access site: url_3
총 걸린시간: 2.002387046813965

위 결과도 2초가 소요된다!

다만 asyncio.gather 함수는 각 태스크들을 unpacked 형태로 넣어주어야 한다. 즉, asyncio.gather(access_site("url_1"), acess_site("url_2")) 또는,

asyncio.gather(*[access_site("url_1"), access_site("url_2")] 처럼 넣어주어야 한다. 나는 다음과 같이 for loop을 이용해서 간편하게 unpacked 형태로 등록해주었다.

 

 

728x90
반응형

댓글