38. asyncio
1. 키워드
- 동기(Synchronous)와 비동기(Asynchronous)
asyncio
(Asynchronous I/O)- 퓨처(Future)와 태스크(Task)
- 블로킹 I/O(Blocking I/O) 함수
2. asyncio
사용하기
asyncio
는 비동기 프로그래밍을 위한 모듈이며 CPU 작업과 I/O를 병렬로 처리하게 해준다.- 동기 처리는 특정 작업이 끝나면 다음 작업을 처리하는 순차처리 방식이고, 비동기 처리는 여러 작업을 처리하도록 예약한 뒤 작업이 끝나면 결과를 받는 방식이다.
1) 네이티브 코루틴 만들기
- 먼저
asyncio
를 사용하려면 다음과 같이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 def
로hello
를 만든다. - 그다음에
asyncio.get_event_loop
함수로 이벤트 루프를 얻고loop.run_until_complete(hello())
와 같이run_until_complete
에 코루틴 객체를 넣는다. - 이때
()
(괄호)로 네이티브 코루틴을 호출하면 코루틴 객체가 생성된다.
run_until_complete
는 네이티브 코루틴이 이벤트 루프에서 실행되도록 예약하고, 해당 네이티브 코루틴이 끝날 때까지 기다린다.- 이렇게 하면 이벤트 루프를 통해서
hello
코루틴이 실행된다. - 할 일이 끝났으면
loop.close
로 이벤트 루프를 닫아준다.
2) await
로 네이티브 코루틴 실행하기
- 이번에는
await
로 네이티브 코루틴을 실행하는 방법이다. - 다음과 같이
await
뒤에 코루틴 객체, 퓨처 객체, 태스크 객체를 지정하면 해당 객체가 끝날 때까지 기다린 뒤 결과를 반환한다. await
는 단어 뜻 그대로 특정 객체가 끝날 때까지 기다린다.await
키워드는 파이썬 3.5 이상부터 사용 가능하고, 3.4에서는yield from
을 사용한다.
- 여기서 주의할 점이 있는데,
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 def
와await
는 파이썬 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.request
의urlopen
으로 웹 페이지를 가져온 뒤 웹 페이지의 길이를 출력해 보자.
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
도 네이티브 코루틴이므로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.gather
함수에 넣어준다. asyncio.gather
는 모든 코루틴 객체(퓨처, 태스크 객체)가 끝날 때까지 기다린 뒤 결과(반환값)를 리스트로 반환한다.
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
를 사용하여 비동기로 가져올 때를 비교해 보면 다음과 같은 모양이 된다.
run_in_executor
에 키워드 인수를 사용하는 함수 넣기
run_in_executor
같은 함수는 위치 인수만 넣을 수 있는데 파이썬에서는 키워드 인수를 많이 사용한다.run_in_executor
에 키워드 인수를 사용하는 함수를 넣을 때는functools.partial
을 사용해야 한다.functools.partial
은 이름 그대로 부분 함수를 만들어주는 기능이다.
import functools
async def hello(executor):
await loop.run_in_executor(None, functools.partial(print, "Hello", "Python", end=" "))
functools.partial
은 인수가 포함된 부분 함수를 반환하는데, 반환된 함수에 다시 인수를 지정해서 호출할 수 있다.
5) async with
사용하기
async with
은 클래스나 함수를 비동기로 처리한 뒤 결과를 반환하는 문법이다.async with
은with
다음에 클래스의 인스턴스를 지정하고as
뒤에 결과를 저장할 변수를 지정한다.async with
은 파이썬 3.5 이상부터 사용 가능하다.
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.a
와self.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
가 출력된다.
비동기 표현식
async
와await
를 사용하면 리스트, 딕셔너리, 세트, 제너레이터 표현식을 비동기 표현식으로 만들 수 있다.- 비동기 표현식은 파이썬 3.6 이상부터 사용 가능하다.
리스트 = [변수 async for 변수 in 비동기이터레이터()]
딕셔너리 = {키: 값 async for 키, 값 in 비동기이터레이터()}
세트: {변수 async for 변수 in 비동기이터레이터()}
제너레이터 = (변수 async for 변수 in 비동기이터레이터())
- 다음과 같이 표현식 안에서
await
로 코루틴을 실행할 수도 있다. - 여기서는 리스트 표현식을 예로 들었지만 딕셔너리, 세트, 제너레이터 표현식 안에서도
await
를 사용할 수 있다.