[Python] 비동기 함수 - asyncio

Hailey Park·2022년 1월 17일
1

Python!

목록 보기
11/11
post-thumbnail

동기와 비동기 함수

  • 동기 함수(sync function) : 함수가 완료될 때까지 리턴하지 않는다. 함수를 호출하게 되면 함수의 처음부터 진행하다 함수의 끝에 도달하거나, 도중에 return문을 만나게 되면 함수는 종료되고, 제어권은 다시 호출자에게 되돌아간다. 이 경우 호출자는 자신이 호출한 함수가 종료하고 리턴할 때까지 기다리고, 함수가 리턴했다는 것은 호출된 함수의 실행이 완료되었다는 것을 보장한다.

  • 비동기 함수 (async function) : 함수 완료 여부와 상관없이 호출자에게 리턴하며, 작업이 완료되면 호출자에게 완료를 통보한다. 함수를 호출하면 실행이 완료되지 않더라도 호출자에게 리턴, 즉, 제어권을 넘기고 자기 혼자 백그라운드로 작업을 계속 한다. 그리고 어느 순간 작업이 완료되면 호출자에게 작업이 완료되었음을 '통보'해준다.

일반적인 비동기 함수 예시

import time

def example_func() :
    print('python', end=' ', flush=True)
    time.sleep(1)
    print('study')
    
print(f"started at {time.strftime('%X')}")
example_func()  # example_func() 함수가 종료될 때까지 기다림
print(f"finish at {time.strftime('%X')}")  # 여기까지 왔다는 것은 foo()가 완벽히 종료되었음을 의미

예제 1. 동기 방식 예제

*실제 웹페이지를 다운 받으면 대부분 너무 빨라 시간 비교가 어려워 웹페이지 다운로드 대신 sleep()함수를 호출

import time

def download_page(url):
    time.sleep(1) #페이지 다운로드
    #html 분석
    print("complete download :", url)

def main():
    download_page("url_1")
    download_page("url_2")
    download_page("url_3")
    download_page("url_4")
    download_page("url_5")

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

output

started at 17:35:11
complete download : url_1
complete download : url_2
complete download : url_3
complete download : url_4
complete download : url_5
finish at 17:35:16

동기 함수는 작업을 완료하고 리턴을 해야 다음 작업을 진행할 수 있는데, sleep()함수 때문에 매번 download_page() 호출 마다 1초씩 아무것도 하지 않고 멈추는 시간 때문에 프로그램을 완료하는데 5초가 넘는 시간이 걸린다.

예제 2. 비동기 함수 사용

import time
import asyncio

#def async_download_page(url) :
async def async_download_page(url) : #async def 키워드로 대체
    await asyncio.sleep(1)
    #html 분석
    print("complete download:", url)
    
#def main()
async def main() : 
    await async_download_page("url_1")
    await async_download_page("url_2")
    await async_download_page("url_3")
    await async_download_page("url_4")
    await async_download_page("url_5")

print(f"started at {time.strftime('%X')}")
asyncio.run(main())
print(f"finish at {time.strftime('%X')}")    

output

started at 17:43:34
complete download: url_1
complete download: url_2
complete download: url_3
complete download: url_4
complete download: url_5
finish at 17:43:39
  • 비동기 함수가 바로 리턴해버리면서 완료되지 않은 결과에 접근하는 것을 방지하기 위해 비동기 함수(asyncio.sleep) 호출 앞에 await 키워드를 사용한다.

  • 함수 또는 객체가 await 표현식에서 사용될 수 있을 때, 이를 어웨이터블 객체라고 말한다.

  • await 키워드는 asyncio.sleep() 함수가 리턴하면 바로 다음 코드를 진행하지 말고, OS로부터 완료 통보가 올 때까지 기다리라는 의미이다.

  • await를 이용하면 요청했던 비동기 작업의 완료 통보가 올 때까지 기다린다. 아무것도 하지 않으면서 기다리는 것이 아니고 "이벤트 루프"를 먼저 확인 후에 이벤트 루프에 일거리가 있다면 그 일들을 처리하면서 기다린다.

  • 이벤트 루프란?
    이벤트 루프는 파일 읽기/쓰기, 타이머, 네트워크 IO같은 비동기 함수(작업)들을 등록하면 내부적으로 루프를 돌며 등록된 작업들을 하나씩 실행하고 완료 시 그 결과를 통보해준다.

  • 위 예제 2번의 main()함수 안의 download_page() 함수의 호출 앞에는 모두 await 키워드가 붙어있다. 이는 이전의 download_page() 함수 호출로부터 완료 통보가 떨어지기 전까지는 다음 download_page() 호출로 진행되지 않는다는 뜻이다.

예제 2-1. asyncio.gather() 사용

import time
import asyncio

#def async_download_page(url) :
async def async_download_page(url) : #async def 키워드로 대체
    await asyncio.sleep(1)
    #html 분석
    print("complete download:", url)
    
#def main()
async def main() : 
    await asyncio.gather(
    async_download_page("url_1"),
    async_download_page("url_2"),
    async_download_page("url_3"),
    async_download_page("url_4"),
    async_download_page("url_5")
    )
    
print(f"started at {time.strftime('%X')}")
asyncio.run(main())
print(f"finish at {time.strftime('%X')}") 

output

started at 17:55:11
complete download: url_1
complete download: url_2
complete download: url_3
complete download: url_4
complete download: url_5
finish at 17:55:12
  • asyncio.gather() : asyncio 모듈에서는 여러 비동기 함수를 한번에 등록할 수 있는 gather() 함수를 제공한다.

  • 예제 2-1의 결과로, main()에서 시작된 첫번째 async_download_page("url_1") 함수는 실행과 동시에 리턴하고, 이벤트 루프에 있는 다른 비동기 함수를 실행한다. 이런 식으로 모든 async_download_page() 함수들을 호출하고, 모든 비동기 함수가 완료되면 async.run() 함수는 그 결과를 모아 한번에 리턴하고 프로그램이 종료된다.

예제 3. 동기 방식 - 웹페이지 다운로드

import time
import requests

def download_page(url):
    req = requests.get(url)   # 동기 함수로 웹페이지 다운로드
    html = req.text
    print("complete download:", url, ", size of page(", len(html),")")
    
def main() :
    download_page("https://www.python.org/")
    download_page("https://www.python.org/")
    download_page("https://www.python.org/")
    download_page("https://www.python.org/")
    download_page("https://www.python.org/")
    
print(f"started at {time.strftime('%X')}")

start_time = time.time()
main()
finish_time = time.time()

print(f"finish at {time.strftime('%X')}, total : {finish_time-start_time} sec(s)") 

output

started at 18:00:59
complete download: https://www.python.org/ , size of page( 49857 )
complete download: https://www.python.org/ , size of page( 49857 )
complete download: https://www.python.org/ , size of page( 49857 )
complete download: https://www.python.org/ , size of page( 49857 )
complete download: https://www.python.org/ , size of page( 49857 )
finish at 18:00:59, total : 0.5474498271942139 sec(s)
  • requests.get()은 동기화 함수이다. (pip install requests 로 모듈 인스톨 해야함.)

예제 4. 비동기 방식 - 웹페이지 다운로드

import time
import requests
import asyncio

async def download_page(url):
    loop = asyncio.get_event_loop()    # 이벤트 루프 객체 얻기
    req = await loop.run_in_executor(None, requests.get, url) # 동기함수를 비동기로 호출
    
    html = req.text
    print("complete download:", url, ", size of page(", len(html),")")
    
async def main() :
    await asyncio.gather(
    download_page("https://www.python.org/"),
    download_page("https://www.python.org/"),
    download_page("https://www.python.org/"),
    download_page("https://www.python.org/"),
    download_page("https://www.python.org/")
    )
    
print(f"started at {time.strftime('%X')}")

start_time = time.time()
asyncio.run(main())
finish_time = time.time()

print(f"finish at {time.strftime('%X')}, total : {finish_time-start_time} sec(s)") 

output

started at 18:03:39
complete download: https://www.python.org/ , size of page( 49857 )
complete download: https://www.python.org/ , size of page( 49857 )
complete download: https://www.python.org/ , size of page( 49857 )
complete download: https://www.python.org/ , size of page( 49857 )
complete download: https://www.python.org/ , size of page( 49857 )
finish at 18:03:39, total : 0.0934300422668457 sec(s)
  • 파이썬에서는 동기 함수를 비동기로 동작할 수 있도록 이벤트 루프에서 run_in_executor() 함수를 제공한다.

    awaitable loop.run_in_executor(executor, func, *args)

    지정된 executor에서 func가 호출되도록 등록한다. executor가 None이면 기본 executor가 사용된다.

  • run_in_executor() 함수를 사용하기 위해서는 먼저 이벤트 루프 객체가 필요하므로 asyncio.get_event_loop()를 통해 이벤트 루프 객체를 얻어와야 한다.

  • 실행시간이 0.54초에서 0.09초로 줄어들었다.

  • event_loop() 더보기

Reference

https://kukuta.tistory.com/345

profile
I'm a deeply superficial person.

0개의 댓글