Skip to content

38. asyncio


1. 키워드

  • 동기(Synchronous)와 비동기(Asynchronous)
  • asyncio(Asynchronous I/O)
  • 퓨처(Future)와 태스크(Task)
  • 블로킹 I/O(Blocking I/O) 함수


2. asyncio 사용하기

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


001


1) 네이티브 코루틴 만들기

  • 먼저 asyncio를 사용하려면 다음과 같이 async def로 네이티브 코루틴을 만든다.
  • 파이썬에서는 제너레이터 기반의 코루틴과 구분하기 위해 async def로 만든 코루틴은 네이티브 코루틴이라고 한다.


async def 함수이름():
    코드
import asyncio

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

loop = asyncio.get_event_loop() # 이벤트 루프를 얻음
loop.run_until_complete(hello()) # hello가 끝날 때까지 기다림
loop.close() # 이벤트 루프를 닫음

# Hello, world!


  • 실행을 해보면 'Hello, world!'가 출력된다.


  • 위 코드의 과정을 살펴보면 먼저 async defhello를 만든다.
  • 그다음에 asyncio.get_event_loop 함수로 이벤트 루프를 얻고 loop.run_until_complete(hello())와 같이 run_until_complete에 코루틴 객체를 넣는다.
  • 이때 ()(괄호)로 네이티브 코루틴을 호출하면 코루틴 객체가 생성된다.


이벤트루프 = asyncio.get_event_loop()
이벤트루프.run_until_complete(코루틴객체또는퓨처객체)
loop = asyncio.get_event_loop() # 이벤트 루프를 얻음
loop.run_until_complete(hello()) # hello가 끝날 때까지 기다림


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


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

  • 이번에는 await로 네이티브 코루틴을 실행하는 방법이다.
  • 다음과 같이 await 뒤에 코루틴 객체, 퓨처 객체, 태스크 객체를 지정하면 해당 객체가 끝날 때까지 기다린 뒤 결과를 반환한다.
  • await는 단어 뜻 그대로 특정 객체가 끝날 때까지 기다린다.
  • await 키워드는 파이썬 3.5 이상부터 사용 가능하고, 3.4에서는 yield from을 사용한다.


변수 = await 코루틴객체
변수 = await 퓨처객체
변수 = await 태스크객체


  • 여기서 주의할 점이 있는데, await는 네이티브 코루틴 안에서만 사용할 수 있다.


  • 그럼 두 수를 더하는 네이티브 코루틴을 만들고 코루틴에서 1초 대기한 뒤 결과를 반환해 보자.


import asyncio

async def add(a, b):
    print(f"add: {a} + {b}")
    await asyncio.sleep(1.0) # 1초 대기. asyncio.sleep도 네이티브 코루틴
    return a + b # 두 수를 더한 결과 반환

async def print_add(a, b):
    result = await add(a, b) # await로 다른 네이티브 코루틴을 실행하고 반환값을 변수에 저장
    print(f"print_add: {a} + {b} = {result}")

loop = asyncio.get_event_loop() # 이벤트 루프를 얻음
loop.run_until_complete(print_add(1, 2)) # print_add가 끝날 때까지 이벤트 루프를 실행
loop.close() # 이벤트 루프를 닫음

# add: 1 + 2
# print_add: 1 + 2 = 3


  • add: 1 + 2가 출력되고 1초 뒤에 print_add: 1 + 2 = 3이 출력된다.


  • 먼저 print_add부터 살펴보자.
  • print_add에서는 await를 실행하고 반환값을 변수에 저장했다.
  • 이렇게 코루틴 안에서 다른 코루틴을 실행할 때는 await를 사용한다.


async def print_add(a, b):
    result = await add(a, b) # await로 다른 네이티브 코루틴을 반환값을 변수에 저장
    print(f"print_add: {a} + {b} = {result}")


  • add에서는 await asyncio.sleep(1.0)으로 1초 대기한 뒤 return a + b로 두 수를 더한 결과를 반환한다.
  • 사실 await asyncio.sleep(1.0)은 없어도 되지만 코루틴이 비동기로 실행되는 모습을 확인하기 위해 사용했다.
  • 특히 asyncio.sleep도 네이티브 코루틴이라 await를 사용해야 한다.


async def add(a, b):
    print(f"add: {a} + {b}")
    await asyncio.sleep(1.0) # 1초 대기. asyncio.sleep도 네이티브 코루틴
    return a + b # 두 수를 더한 결과 반환


퓨처와 태스크

  • 퓨처(asyncio.Future)는 미래에 할 일을 표현하는 클래스인데 할 일을 취소하거나 상태 확인, 완료 및 결과 설정에 사용한다.
  • 태스크(asyncio.Task)는 asyncio.Future의 파생 클래스이며 asyncio.Future의 기능과 실행할 코루틴의 객체를 포함하고 있다.
  • 태스크는 코루틴의 실행을 취소하거나 상태 확인, 완료 및 결과 설정에 사용한다.


파이썬 3.4 이하에서 asyncio 사용하기

  • async defawait는 파이썬 3.5에서 추가되었다.
  • 따라서 3.5 미만 버전에서는 사용할 수 없기 때문에 다음과 같이 @asyncio.coroutine 데코레이터로 네이티브 코루틴을 만든다.


```python
import asyncio

@asyncio.coroutine
async def 함수이름():
    코드
```

<br/>

- 또한 `await`가 아닌 `yield from`을 사용한다.

<br/>

```python
변수 = yield from 코루틴객체
변수 = yield from 퓨처객체
변수 = yield from 태스크객체
```

<br/>

- 파이썬 3.3에서 `asyncio`는 `pip install asyncio`로 `asyncio`를 설치한 뒤 `@asyncio.coroutine` 데코레이터와 `yield from`을 사용하면 된다.
- 단, 3.3 미만 버전에서는 `asyncio`를 지원하지 않는다.


3) 동기로 웹 페이지 가져오기

  • 먼저 비교를 위해 다음과 같이 asyncio를 사용하지 않고 웹 페이지를 순차적으로 가져온다.
  • urllib.requesturlopen으로 웹 페이지를 가져온 뒤 웹 페이지의 길이를 출력해 보자.


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

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

begin = time()
result = []

for url in urls:
    request = Request(url, headers={"User-Agent": "Mozilla/5.0"}) # UA가 없으면 403 에러 발생
    response = urlopen(request)
    page = response.read()
    result.append(len(page))

print(result)
end = time()
print(f"실행 시간: {end - begin:.3f}초")

# [89590, 88723, 88802, 90142, 90628, 92663]
# 실행 시간: 8.422초


  • 실행을 해보면 웹 페이지의 크기가 출력되고 실행 시간은 약 8초가 걸린다.
  • 여기서는 urls에 저장된 순서대로 주소에 접근해서 웹 페이지를 가져오도록 만들었다.
  • 이렇게 하면 웹 페이지 하나를 완전히 가져온 뒤에 다음 웹 페이지를 가져와야 해서 비효율적이다.


4) 비동기로 웹 페이지 가져오기

  • 그럼 asyncio를 사용해서 비동기로 실행해 보자.


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

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

async def fetch(url):
    request = Request(url, headers={"User-Agent": "Mozilla/5.0"}) # UA가 없으면 403 에러 발생
    response = await loop.run_in_executor(None, urlopen, request) # run_in_executor 사용
    page = await loop.run_in_executor(None, response.read) # run_in_executor 사용
    return len(page)

async def main():
    futures = [asyncio.ensure_future(fetch(url)) for url in urls] # 태스크(퓨처) 객체를 리스트로 만듦
    result = await asyncio.gather(*futures) # 결과를 한꺼번에 가져옴
    print(result)

begin = time()
loop = asyncio.get_event_loop() # 이벤트 루프를 얻음
loop.run_until_complete(main()) # main이 끝날 때까지 기다림
loop.close() # 이벤트 루프를 닫음
end = time()
print(f"실행 시간: {end - begin:.3f}초")

# [89556, 88682, 89925, 90164, 90513, 93965]
# 실행 시간: 1.737초


  • asyncio를 사용하니 실행 시간이 8초대에서 1초대로 줄었다.
  • urlopen이나 response.read 같은 함수(메서드)는 결과가 나올 때까지 코드 실행이 중단(Block)되는데 이런 함수들을 블로킹 I/O 함수라고 부른다.
  • 특히 네이티브 코루틴 안에서 블로킹 I/O 함수를 실행하려면 이벤트 루프의 run_in_executor 함수를 사용하여 다른 스레드에서 병렬로 실행시켜야 한다.


  • run_in_executor의 첫 번째 인수는 Executor인데 함수를 실행시켜 줄 스레드 풀 또는 프로세스 풀을 의미한다.
  • 여기서는 None을 넣어서 기본 스레드 풀을 사용한다.
  • 그리고 두 번째 인수에는 실행할 함수를 넣고 세 번째 인수부터는 실행할 함수에 들어갈 인수를 차례대로 넣어준다.


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


  • run_in_executor도 네이티브 코루틴이므로 await로 실행한 뒤 결과를 가져온다.


async def fetch(url):
    request = Request(url, headers={"User-Agent": "Mozilla/5.0"}) # UA가 없으면 403 에러 발생
    response = await loop.run_in_executor(None, urlopen, request) # run_in_executor 사용
    page = await loop.run_in_executor(None, response.read) # run_in_executor 사용
    return len(page)


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


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


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


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


  • asyncio.gather는 리스트가 아닌 위치 인수로 객체를 받으므로 태스크 객체를 리스트로 만들었다면 asyncio.gather(*futures)와 같이 리스트를 언패킹해서 넣어준다.
  • 또한, asyncio.gather도 코루틴이므로 await로 실행한 뒤 결과를 가져온다.


async def main():
    futures = [asyncio.ensure_future(fetch(url)) for url in urls] # 태스크(퓨처) 객체를 리스트로 만듦
    result = await asyncio.gather(*futures) # 결과를 한꺼번에 가져옴
    print(result)


  • 참고로 asyncio.gather에 퓨처 객체를 넣은 순서와 결과 리스트에서 요소의 순서는 일치하지 않을 수도 있다.


  • 웹 페이지를 순서대로 가져올 때와 asyncio를 사용하여 비동기로 가져올 때를 비교해 보면 다음과 같은 모양이 된다.


002


run_in_executor에 키워드 인수를 사용하는 함수 넣기

  • run_in_executor 같은 함수는 위치 인수만 넣을 수 있는데 파이썬에서는 키워드 인수를 많이 사용한다.
  • run_in_executor에 키워드 인수를 사용하는 함수를 넣을 때는 functools.partial을 사용해야 한다.
  • functools.partial은 이름 그대로 부분 함수를 만들어주는 기능이다.


functools.partial(함수, 위치인수, 키워드인수)
import functools

async def hello(executor):
    await loop.run_in_executor(None, functools.partial(print, "Hello", "Python", end=" "))


  • functools.partial은 인수가 포함된 부분 함수를 반환하는데, 반환된 함수에 다시 인수를 지정해서 호출할 수 있다.


>>> import functools

>>> hello = functools.partial(print, "Hello", "Python", end=" ")

>>> hello()
Hello Python
>>> hello("Script", sep="-")
Hello-Python-Script


5) async with 사용하기

  • async with은 클래스나 함수를 비동기로 처리한 뒤 결과를 반환하는 문법이다.
  • async withwith 다음에 클래스의 인스턴스를 지정하고 as 뒤에 결과를 저장할 변수를 지정한다.
  • async with은 파이썬 3.5 이상부터 사용 가능하다.


async with 클래스() as 변수:
    코드


  • async with으로 동작하는 클래스를 만들려면 __aenter____aexit__ 메서드를 구현해야 한다.
  • 그리고 메서드를 만들 때는 반드시 async def를 사용한다.


class 클래스이름:
    async def __aenter__(self):
        코드

    async def __aexit__(self, exc_type, exc_value, traceback):
        코드


  • 그럼 1초 뒤에 덧셈 결과를 반환하는 클래스를 만들어보자.


import asyncio

class AsyncAdd:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    async def __aenter__(self):
        await asyncio.sleep(1.0)
        return self.a + self.b # __aenter__에서 값을 반환하면 as에 지정한 변수에 들어감

    async def __aexit__(self, exc_type, exc_value, traceback):
        pass

async def main():
    async with AsyncAdd(1, 2) as result: # async with에 클래스의 인스턴스 지정
        print(result) # 3

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()


  • __aenter__ 메서드에서 1초 대기한 뒤 self.aself.b를 더한 결과를 반환하도록 만든다.
  • 이렇게 __aenter__에서 값을 반환하면 as에 지정한 변수에 들어간다.
  • __aexit__ 메서드는 async with as를 완전히 벗어나면 호출되는데 여기서는 특별히 만들 부분이 없으므로 pass를 넣는다.


6) async for 사용하기

  • async for는 비동기로 반복하는 문법이다.
  • async for로 동작하는 클래스를 만들려면 __aiter____anext__ 메서드를 구현해야 하고, 메서드를 만들 때는 반드시 async def를 사용한다.
  • async for는 파이썬 3.5 이상부터 사용 가능하다.


  • 다음은 1초마다 숫자를 생성하는 비동기 이터레이터이다.


import asyncio

class AsyncCounter:
    def __init__(self, stop):
        self.current = 0
        self.stop = stop

    def __aiter__(self):
        return self

    async def __anext__(self):
        if self.current < self.stop:
            await asyncio.sleep(1.0)
            r = self.current
            self.current += 1
            return r
        else:
            raise StopAsyncIteration

async def main():
    async for i in AsyncCounter(3): # for 앞에 async를 붙임
        print(i, end=" ")

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

# 0 1 2


  • 메서드가 __anext__, __aiter__라는 점만 다를 뿐 일반적인 이터레이터와 만드는 방법과 같다.
  • 반복을 끝낼 때는 StopAsyncIteration 예외를 발생시키면 된다.
  • 물론 네이티브 코루틴을 사용할 때는 앞에 await를 붙인다.
  • 비동기 이터레이터를 다 만들었다면 네이티브 코루틴 안에서 async for i in AsyncCounter(3):과 같이 async for에 사용하면 된다.


제너레이터 방식으로 비동기 이터레이터 만들기

  • yield를 사용하여 제너레이터 방식으로 비동기 이터레이터를 만들 수도 있다.
  • 다음과 같이 async def로 네이티브 코루틴을 만들고 yield를 사용하여 값을 바깥으로 전달하면 된다.
  • 이는 파이썬 3.6 이상부터 사용 가능하다.


import asyncio

async def async_counter(stop): # 제너레이터 방식으로 만들기
    n = 0
    while n < stop:
        yield n
        n += 1
        await asyncio.sleep(1.0)

async def main():
    async for i in async_counter(3): # for 앞에 async를 붙임
        print(i, end=" ")

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

# 0 1 2


  • 실행을 해보면 1초 간격으로 0, 1, 2가 출력된다.


비동기 표현식

  • asyncawait를 사용하면 리스트, 딕셔너리, 세트, 제너레이터 표현식을 비동기 표현식으로 만들 수 있다.
  • 비동기 표현식은 파이썬 3.6 이상부터 사용 가능하다.


리스트 = [변수 async for 변수 in 비동기이터레이터()]
딕셔너리 = {:  async for ,  in 비동기이터레이터()}
세트: {변수 async for 변수 in 비동기이터레이터()}
제너레이터 = (변수 async for 변수 in 비동기이터레이터())
async def main():
    a = [i async for i in AsyncCounter(3)]
    print(a) # [0, 1, 2]


  • 다음과 같이 표현식 안에서 await로 코루틴을 실행할 수도 있다.
  • 여기서는 리스트 표현식을 예로 들었지만 딕셔너리, 세트, 제너레이터 표현식 안에서도 await를 사용할 수 있다.


[await 코루틴함수() for 코루틴함수 in 코루틴함수리스트]
async def async_one():
    return 1

async def main():
    coroutines = [async_one, async_one, async_one]
    a = [await co() for co in coroutines]
    print(a) # [1, 1, 1]

References