Search

Python Async Programing

1. 주요 개념

가. I/O Bound 성능 이슈

1) 원인

복수의 외부서비스(HTTP Response) 처리 작업을 하나의 쓰레드에 할당되면 하나하나의 외부서비스(HTTP Response)의 응답을 끝까지 기다리고 다음 응답을 순차적으로 처리하여 매우 긴 대기시간이 소요됨
외부 서비스(external service)에 HTTP Request에 따른 응답을 받아오는 작업이 여러개일 때, 이 작업을 하나의 쓰레드에 할당되고 각 외부 서비스의 모든 response를 받아와야만 작업이 종료됨

2) 해결 방법

asyncio 라이브러리 활용: event loop와 coroutine(lightweight thread)에 의해IO bound 작업의 경우, 실행 후 응답 받는 시간 동안에 다른 작업 실행
threading 라이브러리 활용: 하나의 쓰레드에서 모든 외부서비스의 응답을 다 기다리지 않고, context switching에 따라 각각 쓰레드에게 외부 서비스의 응답을 기다리도록 분배함
image from www.youtube.com/watch?v=YlnvCIZQDkw&list=PLBrGAFAIyf5rby7QylRc6JxU5lzQ9c4tN&index=7

나. concurrency vs parallelism

다. generator

1) Iterator

__iter__()와 __next__()를 토대로 반복문에서 순회가 가능한 객체
→ __iter__(): 반복문에서 iterator 반환
→ __next__(): iterator 내부 속성을 토대로 순차 계산
class Counter(object): def __iter__(self): iter = Iterator() return iter class Iterator(object): def __init__(self): self.index = 0 def __next__(self): if self.index > 10: raise StopIteration n = self.index * 2 self.index += 1 return n
Python
복사
code from https://blog.humminglab.io/posts/python-coroutine-programming-1/#iterator
>>> c = Counter() >>> for i in c: print(i) # 0~20까지 짝수 출력
Python
복사
code from https://blog.humminglab.io/posts/python-coroutine-programming-1/#iterator

2) yield & generator

yield, generator는 메모리에 한 번에 올리기에 부적합한 대용량 및 스트림 데이터 처리에 적합
yield: generator 객체 반환
generator: 데이터를 한 번에 생성하지 않고 필요에 따라 데이터를 생성하는 객체
yield vs return
→ yield는 generator 객체를 반환함
→ 반환된 generator 객체는 한 번에 값을 반환하지 않고 print 명령 실행 시점에서 char 출력이 필요할 때마다 값을 하나씩 생성함
def yield_each_char(input: str): for char in input: yield char # print(yield_each_char("Hello")) # <generator object yield_each_char at 0x1059f6890> for char in yield_each_char("Hello"): print(f'result: {char}') # result: H # result: e # result: l # result: l # result: o
Python
복사
def return_each_char(input: str): return list(input) # print(return_each_char("Hello")) # ['H', 'e', 'l', 'l', 'o'] for char in return_each_char("Hello"): print(f'result: {char}')
Python
복사

2) yield from

정의: iterator 또는 다른 generator 대상으로 generator 객체로 반환하는 축약 명령
iterator 대상으로 generator 객체 반환 예시
#def yield_each_char(input: str): # for char in input: # yield char # to def yield_each_char(input: str): yield from input for char in yield_each_char("Hello"): print(f'result: {char}')
Python
복사
code from https://blog.humminglab.io/posts/python-coroutine-programming-1/#generator-yield-%ED%82%A4%EC%9B%8C%EB%93%9C
다른 generator(또는 subcoroutine) 대상 generator 객체 반환 예시
→ generator를 대상으로 next 호출 및 다양한 예외 처리를 생략할 수 있음
yield from에 축약된 코드 참고: https://peps.python.org/pep-0380/
def subcoroutine(): print("Subcoroutine") x = yield 1 print("Recv: " + str(x)) x = yield 2 print("Recv: " + str(x)) # def coroutine(): # _i = subcoroutine() # _x = next(_i) # while True: # _s = yield _x # # if _s is None: # _x = next(_i) # else: # _x = _i.send(_s) # to def coroutine(): yield from subcoroutine() from test import * x = coroutine() next(x) x.send(10) x.send(20)
Python
복사
code from https://blog.humminglab.io/posts/python-coroutine-programming-1/#generator-yield-%ED%82%A4%EC%9B%8C%EB%93%9C
반환값이 추가됨으로써 경량화된 형태의 쓰레드가 될 수 있음
→ return 값 추가하여 parent coroutine에서 반환값을 활용할 수 있음
def sum(max): tot = 0 for i in range(max): tot += i yield tot return tot def coroutine(): x = yield from sum(10) print('Total: {}'.format(x))
Python
복사
code from https://blog.humminglab.io/posts/python-coroutine-programming-1/#generator-yield-%ED%82%A4%EC%9B%8C%EB%93%9C

4) generator comprehension

사용법: list comprehension과 사용법은 유사함. 차이점은 대괄호 대신 소괄호를 사용함
generator = (char for char in "Hello") for char in generator: print(f'result: {char}')
Python
복사

라. coroutine based on yield

1) coroutine이란

Subroutine vs Coroutine
→ SubRoutine: 상위 루틴에 종속되어, 상위 루틴의 종료에 따라 발생하는 반환값에 종속되어 실행되는 로직
→ 하나의 진입점(call)과 하나의 탈출점(return) 존재
→ Coroutine: 각 루틴이 종료되지 않은 시점에서 값을 주고 받음으로써 각각의 루틴에 대해 영향을 줄 수 있는 로직
→ 사용자가 만든 경량 쓰레드로써 OS에 의한 context switching으로부터 자유롭고 자체적인 stack을 통해 데이터 관리 가능
→ 다양한 진입점(call)과 다양한 탈출점(return) 존재

2) 코루틴 장단점

장점
→ OS에 의한 thread context switching에서 자유롭다
→ context switching에 자유롭기 때문에 쓰레드 간 자원동기화에 따른 locking에서도 자유롭다
단점
→ coroutine은 싱글 쓰레드로 동작하기 때문에 멀티 CPU 코어 자원을 활용할 수 없다

3) Coroutine 동작 흐름

next vs send
→ next: coroutine 내 코드를 실행하지만 main routine으로 값을 전달하지 않음
→ send: coroutine 내 코드 실행 후 main routine으로 값 송수신
generator vs coroutine
→ generator: __next__ 메소드 반복 호출에 따른 값 수신
→ coroutine: __next__ 메소드 한 번 호출 후, send로 값 송수신
코드 예시
1) next(cr): coroutine 코드 실행, corouting_test 내 text = (yield greeting) 에서 greeting에 할당된 변수를 main으로 전달 완료 후 대기
→ greeting 변수가 참조하고 있는 값(”hello”) 출력
2) cr.send("morning"): “morning” 값을 coroutine에 전달, print(f"in coroutine, {text}") 실행, text = yield greeting + text greeting + text 값(good morning) 전달 후 대기
3) cr.send("afternoon"): “afternoon” 값을 text에 할당, print(f"in coroutine, {text}") 실행, text = yield greeting + text greeting + text 값(good morning) 전달 후 대기
text = yield greeting + text 에서 yield greeting + text 은 두 가지 역할을 수행함
1.
greeting + text 값(“afternoon”)을 main routine으로 전달함.
2.
main에서 send로 전달한 값을 받아서 변수(text)에 할당함
import time def coroutine_test(): greeting = "good " text = (yield greeting) while True: print(f"in coroutine, {text}") text = yield greeting + text if __name__ == "__main__": cr = coroutine_test() print(f"in main, {cr}") print(f"next 1st, in main, {next(cr)}") time.sleep(1) print("send 1st, main -> co") print(cr.send("morning")) time.sleep(1) print("send 2nd, main -> co") print(cr.send("afternoon")) time.sleep(1) print("send 3nd, main -> co") print(cr.send("evening")) time.sleep(1)
Python
복사
출력 결과
in main, <generator object coroutine_test at 0x104d7c350> next 1st, in main, good send 1st, main -> co in coroutine, morning good morning send 2nd, main -> co in coroutine, afternoon good afternoon send 3nd, main -> co in coroutine, evening good evening
Plain Text
복사

마. coroutine based on asyncio

1) asyncio

정의: async/await 문법 기반으로 coroutine을 작성할 수 있는 비동기 라이브러리
주요 동작원리
asyncio.run() 함수로 메인 루틴 실행
→ await 문법으로 coroutine 함수 대기
→ corouine 함수를 동시적으로 실행하기 위해 asyncio.create_task() 함수를 활용하여 coroutine 함수를 asyncio Tasks로 정의
예시 코드
import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay) print(what) async def main(): task1 = asyncio.create_task( say_after(1, 'hello')) task2 = asyncio.create_task( say_after(2, 'world')) print(f"started at {time.strftime('%X')}") # Wait until both tasks are completed (should take # around 2 seconds.) await task1 await task2 print(f"finished at {time.strftime('%X')}") asyncio.run(main())
Python
복사
code from https://docs.python.org/3/library/asyncio-task.html#

2) awaitable objects

await 표현식으로 호출될 수 있는 객체는 coroutine, future, task가 있음
coroutine: 다른 코루틴 객체에서 호출될 수 있음
→ 반드시 await 명령과 함께 호출해야 함
task: coroutine 객체를 스케줄링하여 동시적으로 실행함
→ coroutine 객체를 wrapping하여 정의되는 객체임
future: low-level object
→ 어플리케이션 단계에서 사용될 일 없음

3) coroutine 실행명령

run: 매개변수로 전달 받은 coroutine 객체를 실행하는 함수
gather: 매개변수로 전달 받은 복수의 coroutine 객체를 task로 지정하여 동시적으로 실행하는 함수
→ 반환값은 리스트의 형태로 전달 받음
import asyncio async def factorial(name, number): f = 1 for i in range(2, number + 1): print(f"Task {name}: Compute factorial({number}), currently i={i}...") await asyncio.sleep(1) f *= i print(f"Task {name}: factorial({number}) = {f}") return f async def main(): # Schedule three calls *concurrently*: L = await asyncio.gather( factorial("A", 2), factorial("B", 3), factorial("C", 4), ) print(L) asyncio.run(main()) # Expected output: # # Task A: Compute factorial(2), currently i=2... # Task B: Compute factorial(3), currently i=2... # Task C: Compute factorial(4), currently i=2... # Task A: factorial(2) = 2 # Task B: Compute factorial(3), currently i=3... # Task C: Compute factorial(4), currently i=3... # Task B: factorial(3) = 6 # Task C: Compute factorial(4), currently i=4... # Task C: factorial(4) = 24 # [2, 6, 24]
Python
복사
code from https://docs.python.org/3/library/asyncio-task.html#

3) eventloop

동작원리

Reference