티스토리 뷰
Asyncio
Asyncio는 python에서 정식으로 제공하는 비동기 논블록 I/O를 위한 모듈입니다.
비동기 논블록 처리는 비용 대비를 따져봤을 때 입출력 작업에 가장 효과적이므로 python에서도 비동기 논블록 모듈을 I/O 작업과 연결지어 Asyncio라는 모듈을 만들었습니다.
Asyncio는 python 3.4.에서 처음으로 추가되었습니다.
이후 새로운 버전마다 Asyncio에 새로운 문법과 기능들이 추가되고 있습니다.
Python 3.4에 추가된 Asyncio를 시작으로 각 버전의 python 비동기 논블록에 대해 살펴봅시다.
Asyncio in Python 3.4
Asyncio에는 비동기 처리를 위한 event loop가 존재합니다.
이 event loop에 coroutine으로 만든 작업을 등록해 비동기 처리를 수행합니다.
Python 3.4에서 사용되는 coroutine은 generator 기반의 coroutine으로 decorator 형태로 사용됩니다.
import asyncio
import random
import datetime
@asyncio.coroutine
def print_time(idx):
sleep_time = random.randrange(1, 10)
yield from asyncio.sleep(sleep_time)
print("[%s] Sleep time : %s, Complete time : %s" % (idx, sleep_time, datetime.datetime.now()))
def main():
futures = [print_time(i) for i in range(10)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(futures))
loop.close()
if __name__ == "__main__":
main()
result :
[4] Sleep time : 1, Complete time : 2021-07-29 02:01:30.487059
[9] Sleep time : 2, Complete time : 2021-07-29 02:01:31.498824
[8] Sleep time : 2, Complete time : 2021-07-29 02:01:31.501900
[6] Sleep time : 3, Complete time : 2021-07-29 02:01:32.494703
[1] Sleep time : 3, Complete time : 2021-07-29 02:01:32.494703
[0] Sleep time : 3, Complete time : 2021-07-29 02:01:32.495702
[7] Sleep time : 3, Complete time : 2021-07-29 02:01:32.496700
[5] Sleep time : 5, Complete time : 2021-07-29 02:01:34.487777
[3] Sleep time : 5, Complete time : 2021-07-29 02:01:34.488773
[2] Sleep time : 8, Complete time : 2021-07-29 02:01:37.498567
예시를 살펴보면 비동기 논블록 방식으로 수행되는 함수는 @asyncio.coroutine decorator를 통해 coroutine으로 정의되었습니다.
함수 내부를 살펴보면 asyncio.sleep 메소드를 볼 수 있습니다.
이 메소드는 인자로 주어진 시간이 지나면 coroutine을 생성해 반환합니다.
yield from은 asyncio.sleep이 완료될 때까지 기다립니다.
main 함수에서는 event loop를 생성합니다.
그리고 asyncio.wait 메소드의 인자로 비동기로 수행할 함수의 list를 등록합니다.
이 메소드는 concurrent.futures에 있는 wait 메소드와 비슷한 역할을 하는 메소드로 인자로 받은 작업이 끝날 때까지 기다리고, 완료된 작업과 완료되지 못한 작업을 분리해 반환합니다.
이때 timeout 인자를 주지 않으면 모든 작업이 마칠 때까지 기다립니다.
asyncio.wait 메소드를 인자로 가지고 있는 run_until_complete 메소드는 인자로 받은 작업이 끝날 때까지 기다리는 역할을 합니다.
즉, futures list에 저장된 모든 작업이 끝날 때까지 기다리게 됩니다.
해당 코드를 실행하면 내부에서는 event loop가 coroutine으로 된 함수를 수행합니다.
이때 각각의 함수들은 I/O작업이나 CPU를 사용하지 않아도 되는 작업을 수행할 때는 제어권을 다른 작업에게 양보하게 됩니다.
Asyncio in Python 3.5
Python 3.5에서 Asyncio에 변화된 사항은 async와 await 문법의 추가입니다.
해당 문법의 추가는 python에 정식으로 coroutine 타입이 추가됨을 나타냅니다.
Python 3.4에서는 decorator와 yield from을 사용해 generator 기반의 coroutine을 사용했습니다.
하지만 python 3.5에서는 generator 기반이 아닌 python 내부적으로 구현한 python native coroutine을 사용합니다.
새롭게 추가된 async 키워드를 함수의 정의 앞에 붙여서 coroutine을 정의하고 await를 사용해 작업이 끝날 때까지 기다립니다.
즉, async가 decorator를 대체하고 await가 yield from을 대체하는 것입니다.
Event loop에서는 native coroutine과 generator 기반 coroutine을 함께 사용할 수 있습니다.
하지만 하나의 coroutine에서는 2가지 문법을 혼용할 수 없습니다.
예를 들어 async로 native coroutine을 정의하면 yield from을 사용할 수 없습니다.
import asyncio
import random
import datetime
async def print_time(idx):
sleep_time = random.randrange(1, 10)
await asyncio.sleep(sleep_time)
print("[%s] Sleep time : %s, Complete time : %s" % (idx, sleep_time, datetime.datetime.now()))
def main():
futures = [print_time(i) for i in range(10)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(futures))
loop.close()
if __name__ == "__main__":
main()
result :
[2] Sleep time : 1, Complete time : 2021-07-29 02:12:52.410573
[5] Sleep time : 2, Complete time : 2021-07-29 02:12:53.402597
[3] Sleep time : 2, Complete time : 2021-07-29 02:12:53.403595
[0] Sleep time : 3, Complete time : 2021-07-29 02:12:54.394775
[9] Sleep time : 3, Complete time : 2021-07-29 02:12:54.395771
[4] Sleep time : 6, Complete time : 2021-07-29 02:12:57.408470
[7] Sleep time : 6, Complete time : 2021-07-29 02:12:57.408470
[8] Sleep time : 7, Complete time : 2021-07-29 02:12:58.399699
[1] Sleep time : 7, Complete time : 2021-07-29 02:12:58.400711
[6] Sleep time : 9, Complete time : 2021-07-29 02:13:00.393186
이번 예시 코드를 살펴보면 python 3.4와 달리 async를 통해 비동기로 수행할 함수를 정의하고 있습니다.
또한 yield from이 아닌 await를 통해 asyncio.sleep의 결과를 기다리는 것을 볼 수 있습니다.
async 문법을 사용하면 for문이나 with 구문도 정의할 수 있습니다.
해당 문법을 살펴보기 전에 몇가지 용어에 대해 살펴봅시다.
asynchronous iterable과 asynchronous iterator 그리고 awaitable입니다.
먼저 awaitable은 await 구문에서 사용될 수 있는 객체입니다.
즉, 이터레이터를 반환하는 coroutine은 awaitable하다고 할 수 있습니다.
awaitable한 객체를 구현하려면 내부적으로 __await__ 메소드를 구현해야하고 이터레이터를 반환해야합니다.
다음으로 asynchronous iterable과 asynchronous iterator를 같이 살펴봅시다.
이는 비동기적으로 수행되는 iterable과 iterator입니다.
따라서 async for는 비동기적으로 처리되는 asynchronous iterable을 대상으로 동작하며 반복문의 결과로 비동기적
으로 수행될 수 있는 asynchronous iterator를 반환합니다.
asynchronous iterable은 __aiter__와 __anext__ 메소드를 작성함으로써 구현할 수 있습니다.
단 __anext__ 메소드 앞에는 async 키워드를 붙여주어야 합니다.
import asyncio
class AsynchronousReader:
def __init__(self, file_name):
self.file_name = file_name
self.file = None
try:
self.file = open(self.file_name, "rb")
except:
print("File open error")
def __aiter__(self):
return self
async def __anext__(self):
value = await self.file_readline()
if value == b"":
raise StopAsyncIteration
return value.decode("utf-8").strip()
async def file_readline(self):
return self.file.readline()
def file_close(self):
self.file.close()
async def read_file(file_name):
async_reader = AsynchronousReader(file_name)
async for value in async_reader:
print(value)
async_reader.file_close()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(read_file("./async_example"))
finally:
loop.close()
위의 예시는 async for을 이용해 파일의 내용을 한줄 씩 읽어 출력하는 코드입니다.
AsynchronousReader class는 파일명을 인자로 받아 해당 파일을 열고 iteration을 통해 한 줄씩 읽어 반환합니다.
이때 핵심이 되는 메소드는 __anext__ 입니다.
위에서 설명했듯이 __anext__ 메소드 앞에는 async 키워드가 붙어야합니다.
async for문에서 값을 순회할 때마다 __anext__가 호출되고 이 메소드의 반환값이 async for문의 반환값이 됩니다.
그리고 async for문을 사용할 때 주의해야할 점이 하나 더 있습니다.
바로 async for문은 async로 선언된 함수에서만 사용할 수 있다는 점입니다.
위의 예시에서 async for을 사용한 read_file은 async을 이용해 선언된 것을 볼 수 있습니다.
main에서는 event loop를 생성해 read_file함수를 비동기로 수행합니다.
해당 코드를 수행해보면 read_file의 인자로 준 파일의 모든 줄을 출력하는 것을 알 수 있습니다.
다음으로는 async with 구문을 살펴보겠습니다.
import asyncio
class AsynchronousReader:
def __init__(self, file_name):
self.file_name = file_name
self.file = None
async def __aenter__(self):
try:
self.file = open(self.file_name, "rb")
except:
print("File open error.")
raise Exception
else:
return self.file
async def __aexit__(self, exc_type, exc_value, traceback):
self.file.close()
async def read_file(file_name):
async with AsynchronousReader(file_name) as af:
for line in af.readline():
print(line.decode("utf-8").strip())
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(read_file("./async_example"))
finally:
loop.close()
이 예시는 위의 async for을 이용한 예시와 같은 역할을 합니다.
다른 점은 async with구문을 사용하기 위해서는 __aenter__ 과 __aexit__ 메소드를 구현해야합니다.
두 메소드는 모두 async 키워드를 붙여야합니다.
__aenter__ 메소드는 with 구문을 사용해 변수를 선언했을 때 변수에 담길 정보를 설정하는 역할을 합니다.
위의 예시에서는 open한 file 객체를 변수에 저장합니다.
__aexit__은 with 구문을 통해 문맥관리가 종료되었을 때 처리할 내용을 설정합니다.
위의 예시에서는 file 객체를 close합니다.
출력결과 async for을 이용한 예시와 같습니다.
Asyncio in Python 3.6
Python 3.5에는 native coroutine에서는 yield from을 사용하지 못하고 generator 기반 coroutine에서는 await를 사용하지 못하는 제약이 있었습니다.
Python 3.6에서는 이러한 제약이 사라졌습니다.
즉, native coroutine에서도 yield from을 사용할 수 있게 되었고 generator도 만들 수 있게 되었습니다.
또한 python 3.6에서는 asynchronous comprehension을 구현할 수 있게 되었습니다.
먼저 native coroutine에서 yield와 await를 같이 사용하는 예시를 살펴봅시다.
import asyncio
async def delay_range(to, delay=1):
for i in range(to):
yield i
await asyncio.sleep(delay)
async def run():
async for i in delay_range(10):
print(i)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(run())
finally:
loop.close()
result :
0
1
2
3
4
5
6
7
8
9
delay_range 함수는 async 키워드를 통해 native coroutine으로 정의되었습니다.
하지만 python 3.6에서 추가된 것 처럼 yield와 await를 같이 사용하고 있습니다.
이때 delay_range는 yield를 통해 값을 반환하므로 iterable합니다.
하지만 이 delay_range를 for을 이용하면 type 오류가 발생합니다.
그 이유는 delay_range 함수는 async로 정의된 Asynchronous iterable 객체이기 때문입니다.
따라서 async 키워드를 이용해 정의된 함수안에서 async for을 사용해서 호출해야합니다.
실행결과는 asyncio.sleep 메소드 때문에 1초 간격으로 값이 출력됩니다.
다음으로는 python 3.6에서 추가된 asynchronous comprehension에 대한 예시를 살펴보겠습니다.
import asyncio
async def delay_range(to, delay=1):
for i in range(to):
yield i
await asyncio.sleep(delay)
async def run():
print("Async Comprehension")
return [i async for i in delay_range(3)]
async def run_multiple():
print("Async Await Comprehension")
func_list = [run, run]
result = [await func() for func in func_list]
print(result)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(run_multiple())
finally:
loop.close()
result :
Async Await Comprehension
Async Comprehension
Async Comprehension
[[0, 1, 2], [0, 1, 2]]
위에 예시 코드를 살펴보면 run과 run_multiple 함수에서 모두 comprehension이 사용되고 있습니다.
하지만 약간 다른점이 있습니다.
run 함수에서는 async for을 사용해 comprehension을 구현한 반면 run_multiple 함수에서는 for과 await를 사용해 comprehension을 구현했습니다.
그 이유는 반복문이 반환하는 값이 다르기 때문입니다.
먼저 run에서 사용한 comprehension을 살펴봅시다.
delay_range 함수는 yield를 통해 값을 반환하고 그 값을 comprehension에서 이용하고 있습니다.
이 값은 integer입니다.
반면에 run_multiple 함수에서 사용하는 comprehension에서는 run 함수를 실행한 결과를 받아서 list에 저장하고 있습니다.
즉, coroutine을 실행시키고 그 결과를 받아 list에 저장하고 있는 것입니다.
그렇기 때문에 coroutine을 반환하는 역할을 하는 await를 사용한 것입니다.
단 주의해야 하는 점은 asynchronous comprehension에서는 native coroutine 문법만 지원하기 때문에 yield from은 사용할 수 없습니다.
코드를 실행해보면 delay_range에서 asyncio.sleep 메소드를 통해 출력시간을 지연시켰기 때문에 run_multiple 함수의 출력이 지연되는 것을 볼 수 있습니다.
Asyncio Task
일반적으로 event loop는 비동기적으로 작업을 수행할 때 사용됩니다.
또한 event loop는 실행할 작업을 등록한 뒤에 작업을 실행하거나, 지연시키거나, 취소할 수 있습니다.
그리고 여러개의 비동기 함수를 한번에 event loop에 등록해 사용합니다.
이를 예약(Schedule)한다고 합니다.
event loop의 등록된 작업중 대기 시간이 필요한 작업(I/O 작업 등)이 있는 경우 해당 작업을 중지하고 다른 작업을 먼저 실행합니다.
그리고 대기 시간이 끝나면 중지되었던 작업을 이어서 수행합니다.
Asyncio에서 작업 관리는 Task 기반으로 이루어집니다.
Task는 future를 wrapping한 것입니다.
future는 향후 실행할 작업을 의미하고 이 작업은 언제 실행될지는 모르지만 실행될 예정이니 예약을 해두는 의미를 가집니다.
asyncio나 event loop에서 제공하는 메소드를 사용하면 coroutine을 task로 변경하여 등록할 수 있습니다.
이처럼 coroutine과 task는 다른 개념입니다.
예를 들어 await에서 task를 사용하면 task가 완료될 때까지 coroutine이 중단됩니다.
반면에 await에 coroutine을 사용하면 다른 coroutine의 반환값을 기다리게 됩니다.
Task 객체는 future에서 사용하는 여러가지 기능을 포함합니다.
즉, 함수의 동작 중에 상태를 확인하거나 취소하는 등의 작업을 수행할 수 있습니다.
import asyncio
import random
import datetime
async def print_time(idx):
sleep_time = random.randrange(1, 5)
await asyncio.sleep(sleep_time)
print("[%s] Sleep time : %s, Complete time : %s" % (idx, sleep_time, datetime.datetime.now()))
return idx
async def tasks():
task_list = [asyncio.ensure_future(print_time(i)) for i in range(10)]
for idx, task in enumerate(task_list):
if idx % 2 == 0:
task.cancel()
print("[%s] task is cancelled" % idx)
else:
task.add_done_callback(callback)
await asyncio.wait(task_list)
def callback(task):
print("[%s] Call callback function" % task.result())
def main():
loop = asyncio.get_event_loop()
loop.run_until_complete(tasks())
loop.close()
if __name__ == "__main__":
main()
result :
[0] task is cancelled
[2] task is cancelled
[4] task is cancelled
[6] task is cancelled
[8] task is cancelled
[3] Sleep time : 1, Complete time : 2021-07-29 17:11:52.334710
[3] Call callback function
[7] Sleep time : 2, Complete time : 2021-07-29 17:11:53.324644
[7] Call callback function
[5] Sleep time : 3, Complete time : 2021-07-29 17:11:54.338171
[1] Sleep time : 3, Complete time : 2021-07-29 17:11:54.339172
[5] Call callback function
[1] Call callback function
[9] Sleep time : 4, Complete time : 2021-07-29 17:11:55.331421
[9] Call callback function
위의 예시에서 보면 asyncio.ensure_future 메소드를 통해 task 객체를 생성하는 것을 볼 수 있습니다.
task 객체를 담은 list를 순회하면서 idx의 값이 짝수이면 cancel 메소드를 통해 해당 작업을 취소합니다.
또한 idx의 값이 홀수이면 callback 함수로 작업의 실행 결과를 반환합니다.
event loop에 작업을 coroutine으로 등록하는 것과 task로 등록하는 것의 결과에는 차이가 없을 수도 있습니다.
하지만 실행 중간에 작업에 어떤 처리를 할 수 있는지 없는지의 차이가 있습니다.
따라서 event loop에 coroutine을 등록할지 task를 등록할지는 상황에 따라 결정하면 됩니다.
reference
프로그래밍 언어의 개념과 흐름에 대한 고찰, 파이썬답게 코딩하기
'Python' 카테고리의 다른 글
[python] 10. 비동기, concurrent.futures (0) | 2021.07.28 |
---|---|
[python] 9. Coroutine (0) | 2021.07.25 |
[python] 8. Multiprocessing (0) | 2021.07.21 |
[python] 7. Thread (2) (0) | 2021.07.18 |
[python] 6. Thread (1) (0) | 2021.07.16 |
- Total
- Today
- Yesterday
- 파이썬문법
- Cryptography
- 암호론
- Qiskit
- 암호화
- 파이썬
- 양자컴퓨터
- 복호화
- Quantum entanglement
- wechall
- 동시발생지수
- Quantum Computing
- 파이썬암호
- systemhacking
- thread
- 시저암호
- 양자컴퓨팅
- 대칭키암호
- pythonic
- 곱셈암호
- 파이썬 문법
- 전치암호
- ACM Computing survey
- 공개키암호
- 덧셈암호
- python
- 파이썬 비동기
- Encrypted Traffic Analysis
- 비대칭키암호
- ETA 프로젝트
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |