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 호출 및 다양한 예외 처리를 생략할 수 있음
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을 작성할 수 있는 비동기 라이브러리
•
주요 동작원리
→ await 문법으로 coroutine 함수 대기
•
예시 코드
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
•
동작원리