[Python] asyncio 사용한 비동기 프로그래밍

Kyunghwan Ko·2022년 11월 12일
0

Python

목록 보기
1/1

asyncio 사용하기

  • asyncio(Asynchronous I/O)는 비동기 프로그래밍을 위한 모듈이며 CPU 작업과 I/O를 병렬로 처리하게 해줌
  • 동기(synchronous) 처리는 특정 작업이 끝나면 다음 작업을 처리하는 순차처리 방식이고, 비동기(asynchronous) 처리는 여러 작업을 처리하도록 예약한 뒤 작업이 끝나면 결과를 받는 방식임

네이티브 코루틴 만들기

  • 파이썬에서는 제너레이터 기반의 코루틴과 구분하기 위해 async def로 만든 코루틴은 네이티브 코루틴이라고 함
  • async def 키워드는 파이썬 3.6이상부터 사용 가능함

예제 코드

import asyncio

# async def로 네이티브 코루틴을 만듦
async def hello():
	print("Hello, World!") 

if __name__ == "__main__":
	loop = asyncio.get_event_loop() # 이벤트 루프를 얻음
    loop.run_until_complete(hello()) # 네이티브 코루틴 객체인 hello()가 끝날 때까지 기다림
    loop.close() # 이벤트 루프를 닫음(relase)

실행결과

HleHello, World!

코드 설명

  • run_until_complete()는 네이티브 코루틴이 이벤트 루프에서 실행되도록 예약하고, 해당 네이티브 코루틴이 끝날 때까지 기다림
  • 이벤트 루프를 통해서 hello() 코루틴이 실행됨
  • 할 일이 끝났으면 loop.close()로 이벤트 루프를 닫아줌

await로 네이티브 코루틴 실행하기

  • 다음과 같이 await 뒤에 코루틴 객체, 퓨쳐객체, 태스크 객체를 지정하면 해당 객체가 끝날 때까지 기다린 뒤 결과를 반환함

변수 = await 코루틴객체

  • await는 단어 뜻 그대로 특정 객체가 끝날 때까지 기다림
  • await 키워드는 파이썬 3.5이상 부터 사용 가능, 3.4에서는 yeild fromd을 사용
  • 여기서 주의할 점, await는 네이티브 코루틴 안에서만 사용할 수 있음

웹 페이지 가져오기(동기 vs 비동기)

동기 방식

  • 아래와 같이 asyncio를 사용하지 않고 웹페이지를 순차적으로 가져오자
  • urlib.request의 urlopen으로 웹 페이지를 가져온뒤 웹 페이지의 길이를 출력해보자

예제 코드

from time import time
from urllib.request import Request, urlopen

if __name__ == "__main__":
	urls = ["https://www.google.co.kr/search?q=" + item
            for item in ["apple", "pear", "grape", "pineapple", "orange", "strawberry"]]
    
    start_time = time()
    results = []
    for url in urls:
        request = Request(url, headers = {"User-Agent": "Mozilla/5.0"}) # UA가 없으면 403 Forbidden 에러 발생
        response = urlopen(request)
        page = response.read()
        results.append(len(page))
        
    print(results)
    end_time = time()
    
    print(f"실행 시간: {round(end_time - start_time, 3)}초")

실행 결과

[81098, 148269, 101121, 75948, 149267, 146069]
실행 시간: 6.078초

비동기 방식

  • asyncio를 사용해서 비동기로 웹페이지를 가져와 보자

예제 코드

from time import time
from urllib.request import Request, urlopen
import asyncio

async def fetch(url):
    request = Request(url, headers={"User-Agent": "Mozilla/5.0"})
    response = await loop.run_in_executor(None, urlopen, request)
    page = await loop.run_in_executor(None, response.read)
    return len(page)

async def main():
    items = [asyncio.ensure_future(fetch(url)) for url in urls]

    results = await asyncio.gather(*items)
    print(results)

if __name__ == "__main__":
    urls = ["https://www.google.co.kr/search?q=" + item
            for item in ["apple", "pear", "grape", "pineapple", "orange", "strawberry"]]

    start_time = time()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main()) # main이 끝날 때 까지 기다림
    loop.close()
    end_time = time()
    print(f"실행 시간: {round(end_time - start_time, 3)}초")

실행 결과

[81097, 148258, 101164, 75948, 149267, 150893]
실행 시간: 1.333초

6초에서 1.3초로 실행시간이 약 78% 빨라진 것을 볼 수 있다.

비동기로 웹페이지 가져오기 코드 설명

async def fetch(url):
    request = Request(url, headers={"User-Agent": "Mozilla/5.0"})
    response = await loop.run_in_executor(None, urlopen, request)
    page = await loop.run_in_executor(None, response.read)
    return len(page)
  • urlopen()이나 response.read() 같은 함수는 결과가 나올 때 까지 코드 실행이 중단(block)되는데 이런 함수들을 Blocking I/O 함수라고 부름

  • 네이티브 코루틴 안에서 Blocking I/O 함수를 실행하려면 이벤트 루프의 run_in_executor()함수를 사용하여 다른 스레드에서 병렬로 실행시켜야 함

  • run_in_executor()의 첫 번재 인수(parameter)는 executor인데 함수를 실행시켜줄 스레드 풀 또는 프로세스 풀임

    이벤트루프.run_in_executor(None, 함수, 인수1, 인수2, 인수3)

  • run_in_executor()도 네이티브 코루틴이므로 await로 실행한 뒤 결과를 가져옴

async def main():
    items = [asyncio.ensure_future(fetch(url)) for url in urls]

    results = await asyncio.gather(*items)
    print(results)
  • main()에서는 네이티브 코루틴 여러 개를 동시에 실행하는데, 이때는 먼저 asyncio.ensure_future() 함수를 사용하여 태스크(asyncio.Task) 객체를 생성하고 리스트로 만들어줌

태스크객체 = asyncio.ensure_future(코루틴객체 또는 퓨처객체)

  • asyncio.gather()는 모든 코루틴 객체(퓨처, 태스크 객체)가 끝날 때까지 기다린 뒤 결과(return 값)를 리스트로 반환함

변수 = await asyncio.gather(코루틴객체1, 코루틴객체2)

  • asyncio.gather()는 리스트가 아닌 위치 인수로 객체를 받으므로 태스크 객체를 리스트로 만들었다면 asyncio.gather(*items)와 같이 리스트를 언패킹(unpacking)해서 넣어줌

  • asyncio.gather()도 코루틴이므로 await로 실행한 뒤 결과를 가져옴

⚠️ 멀티스레드로 동작하기 때문에 반환되는 리스트 결과값이 *items로 전달해준 객체의 순서와 같지 않을 수 있다.

profile
부족한 부분을 인지하는 것부터가 배움의 시작이다.

0개의 댓글