파이썬 코루틴과 비동기 함수
본 포스트는 인프런의 "파이썬 동시성 프로그래밍 : 데이터 수집부터 웹 개발까지 (feat. FastAPI)" 강의를 듣고 회고 목적으로 작성하였습니다.
📌 I/O 바운드 & CPU 바운드, 블로킹
- I/O 바운드
- I는 Input을 의미하고 O는 Output을 의미한다.
- 프로그램이 실행될 때 실행 속도가 I/O에 의해 제한됨을 의미.
- 사용자가 입력을 하고 해당하는 입력에 대해 더하기 100을 한 결과값을 출력해주는 프로그램을 개발한다고 가정해보자.
- 사용자가 입력을 100초 후에 숫자 15를 입력하면 115가 약 100초 후에 출력
- 사용자가 입력을 15초 후에 숫자 3을 입력하면 103이 15초 후에 출력
- 사용자가 입력을 하는 시간에 따라서 프로그램의 실행 속도가 결정된다!!
- 사용자가 키보드로 입력하는 경우 뿐만 아니라, 컴퓨터와 컴퓨터끼리 통신을 할 때에도 I/O 바운드가 발생
- 어떤 프로그램에서 특정 웹에 요청을 하여 응답을 기다리는 코드가 있다고 하면 요청을 하는 것이 I가 되고 응답을 하는 것이 O가 되는 I/O 바운드 인 것이다.
-
CPU 바운드
- 프로그램이 실행될 때 실행 속도가 CPU 속도에 의해 제한됨을 의미한다.
- 정말 복잡한 수학 수식을 계산하는 경우 컴퓨터의 실행속도가 느려진다.
def cpu_bound_func(number: int):
total = 1
arrange = range(1, number + 1)
for i in arrange:
for j in arrange:
for k in arrange:
total *= i * j * k
return total
if __name__ == "__main__":
result = cpu_bound_func(50)
print(result)
-
블로킹
- 바운드에 의해 코드가 멈추게 되는 현상
📌동기 VS 비동기
-
동기(Sync)
- 코드가 동기적으로 동작한다 코드가 반드시 작성된 순서 그대로 실행 된다.
-
비동기(Async)
- 코드가 비동기적으로 동작한다 코드가 반드시 작성된 순서 그대로 실행되는 것이 아니다.
- I는 Input을 의미하고 O는 Output을 의미한다.
- 프로그램이 실행될 때 실행 속도가 I/O에 의해 제한됨을 의미.
- 사용자가 입력을 하고 해당하는 입력에 대해 더하기 100을 한 결과값을 출력해주는 프로그램을 개발한다고 가정해보자.
- 사용자가 입력을 100초 후에 숫자 15를 입력하면 115가 약 100초 후에 출력
- 사용자가 입력을 15초 후에 숫자 3을 입력하면 103이 15초 후에 출력
- 사용자가 입력을 하는 시간에 따라서 프로그램의 실행 속도가 결정된다!!
- 사용자가 키보드로 입력하는 경우 뿐만 아니라, 컴퓨터와 컴퓨터끼리 통신을 할 때에도 I/O 바운드가 발생
- 어떤 프로그램에서 특정 웹에 요청을 하여 응답을 기다리는 코드가 있다고 하면 요청을 하는 것이 I가 되고 응답을 하는 것이 O가 되는 I/O 바운드 인 것이다.
CPU 바운드
- 프로그램이 실행될 때 실행 속도가 CPU 속도에 의해 제한됨을 의미한다.
- 정말 복잡한 수학 수식을 계산하는 경우 컴퓨터의 실행속도가 느려진다.
def cpu_bound_func(number: int): total = 1 arrange = range(1, number + 1) for i in arrange: for j in arrange: for k in arrange: total *= i * j * k return total if __name__ == "__main__": result = cpu_bound_func(50) print(result)
블로킹
- 바운드에 의해 코드가 멈추게 되는 현상
-
동기(Sync)
- 코드가 동기적으로 동작한다 코드가 반드시 작성된 순서 그대로 실행 된다.
-
비동기(Async)
- 코드가 비동기적으로 동작한다 코드가 반드시 작성된 순서 그대로 실행되는 것이 아니다.
동기와 비동기의 차이점을 한 가지 예를 들어서 살펴보겠습니다.
짜장면을 배달하는 배달부가 있습니다. 하나의 주문에서 짜장면 배달부가 처리해야 할 일은 다음과 같습니다.
delivery_func:
1. 짜장면을 주문자에게 배달한다.
2. 해당 주문자가 다 먹은 그릇을 현관 앞에 놔두면 그 그릇을 수거한다.
만약 주문자가 A,B,C 가 있다 하겠습니다.
동기
A에게 배달을 완료한 뒤에 A가 다 먹을 때 까지 기다린다.(A에게 배달을 완료, 요청 I)
20분 후 A가 다 먹은 짜장면 그릇을 현관문에 내놓았다면 그 그릇을 수거한 다음 B에게 배달하러 간다.(A에게 그릇을 받는다, 응답 O)
B에게 배달을 완료한 뒤에 B가 다 먹을 때 까지 기다린다.(B에게 배달을 완료, 요청 I)
13분 후 B가 다 먹은 짜장면 그릇을 수거한 다음 C에게 배달하러 간다.(B에게 그릇을 받는다, 응답 O)
비동기
A에게 배달을 완료하고(요청 I), B에게 배달을 완료한 뒤(요청 I), C에게 배달(요청 I)
이후 A에게 가서 그릇을 수거(응답 O)하고 B 그릇을 수거하고(응답 O), 마지막으로 C 그릇을 수거(응답 O)
이를 코드로 구현하여 확인해보자.
✍ 동기
import time def delivery(name, mealtime): print(f"{name}에게 배달 완료!") time.sleep(mealtime) # mealtime 만큼 시간을 기다리는 함수. print(f"{name} 식사 완료, {mealtime}시간 소요...") print(f"{name} 그릇 수거 완료") def main(): delivery("A", 1) delivery("B", 2) delivery("C", 3) if __name__ == "__main__": start = time.time() print(main()) # None end = time.time() print(end - start)
💻 출력
A에게 배달 완료! A 식사 완료, 1시간 소요... A 그릇 수거 완료 B에게 배달 완료! B 식사 완료, 2시간 소요... B 그릇 수거 완료 C에게 배달 완료! C 식사 완료, 3시간 소요... C 그릇 수거 완료 None 6.0337913036346436
✍ 비동기
import time import asyncio async def delivery(name, mealtime): print(f"{name}에게 배달 완료!") await asyncio.sleep(mealtime) print(f"{name} 식사 완료, {mealtime}시간 소요...") print(f"{name} 그릇 수거 완료") return mealtime async def main(): result = await asyncio.gather( delivery("A", 1), delivery("B", 2), delivery("C", 3), ) print(result) if __name__ == "__main__": start = time.time() asyncio.run(main()) end = time.time() print(end - start)
💻 출력
A에게 배달 완료! B에게 배달 완료! C에게 배달 완료! A 식사 완료, 1시간 소요... A 그릇 수거 완료 B 식사 완료, 2시간 소요... B 그릇 수거 완료 C 식사 완료, 3시간 소요... C 그릇 수거 완료 [1, 2, 3] 3.021336793899536
결과를 보면 비동기 코드의 시간이 더 작은 것을 볼 수 있다.
하지만 이렇다고 비동기 코드가 동기보다 무조건 좋은 것은 아니다.
각각 상황에 따라 좋은 코드가 있을 뿐 이다.
📌파이썬 코루틴의 이해
루틴이란?
일련의 명령 (코드의 흐름)
-
메인 루틴
- 프로그램의 메인 코드의 흐름
-
서브 루틴
- 우리가 알고 있는 보토으이 함수나 메소드 (메인 루틴을 보조하는 역할)
- 하나의 진입점과 하나의 탈출점이 있는 루틴이다.
def hello_world(): # 진입점 print("hello world") return 0 # 탈출점
- 프로그램이 실행되면 서브 루틴은 별도의 공간(스코프)에 해당 로직들을 모아 놓고 있다.
서브루틴이 호출이 될 때, 해당하는 로직들의 공간으로 이동했다가 return을 통해 원래 호출 시점(메인 루틴)으로 돌아오게 된다.
- 코루틴
- 서브 루틴의 일반화된 형태
- 다양한 진입점과 다양한 탈출점이 있는 루틴
- 코루틴은 서브루틴과는 달리 해당 로직들이 진행되는중간에 멈춰서 특정 위치로 돌아 갔다가 다시 코루틴에서 진행되었던 위치로 돌아와 나머지 로직을 수행할 수 있다.
- 파이썬 비동기 함수는 코루틴 함수로 만들 수 있다.
다음은 평소에 많이 쓰는 서브루틴 함수의 구조이다.
def hello_world(): print("Hello World")
이제 def 앞에 async
키워드를 붙이면 이 함수는 코루틴이 된다.
async def hello_world(): print("Hello World")
하지만 이 상태로 함수를 실행시키면 RuntimeWarning이 발생한다. 에러를 잘 살펴보면 hello_world 함수가 awaited 되지 않았다고 한다. await
를 추가해보자.
async def hello_world(): print("Hello World") if __name__ == "__main__": await hello_world() # SyntaxError: 'await' outside function
위 코드를 실행시켜 보면 또 다시 에러가 발생한다. await가 function 밖에서 실행되고 있다는 에러이다. 즉, await
라는 키워드는 코루틴 안에서 사용해야 한다.
자 그렇다면 이 에러를 사라지게 하려면 어떻게 해야할까?
asyncio
라는 패키지를 사용할 수 있다. 아래 코드처럼 asyncio.run()
함수를 사용해보자.
import asyncio async def hello_world(): print("hello world") if __name__ == "__main__": asyncio.run(hello_world()) # hello world
그럼 여기서 궁금한 점이 있을것이다. 위에서 말했던 await
는 언제 사용할까?
await
는 코루틴 내에서 다른 코루틴을 호출하고 그 결과를 받을 때 사용하며, 그 의미는 async
가 붙어서 정의된 함수는 비동기로 호출되는 코루틴이라는 뜻이다. await
는 말 그대로 다른 비동기 코루틴을 호출하되, 해당 작업이 완료될 때까지 기다린다는 뜻으로 해석하면 된다.
다음과 같이 코루틴 안에 print문에 await
를 붙여서 실행하보자.
import asyncio async def hello_world(): await print("hello world") if __name__ == "__main__": asyncio.run(hello_world()) # TypeError: object NoneType can't be used in 'await' expression
그럼 위와 같이 TypeError가 발생한다. 즉, await
객체가 아니라서 발생하는 에러이다.
await
객체는 어웨이터블 이라고 한다.
간단히 말하면 객체가 await 표현식에서 사용될 수 있을 때 어웨이터블 객체라고 말한다.
어웨이터블 객체에는 코루틴, task 및 future가 있다.
파이썬 코루틴은 어웨이터블 이므로 다른 코루틴에서 await를 사용할 수 있고, 코루틴 함수를 호출하여 반환되는 코루틴 객체 또한 사용할 수 있다. 위에서 사용했던 비동기 코드의 일부를 가져와서 살펴보자.
async def delivery(name, mealtime) print(f"{name}에게 배달 완료!") await asyncio.sleep(mealtime) print(f"{name} 식사 완료, {mealtime}시간 소요...") print(f"{name} 그릇 수거 완료") return mealtime async def main(): result = await asyncio.gather( delivery("A", 1), delivery("B", 2), delivery("C", 3), ) print(result)
코드를 보면 delivery
와 main
은 모두 코루틴 함수이다. main
함수를 보면 await asyncio.gather(delivery())
가 포함되어 있는데, delivery
가 코루틴 함수이기 때문에 await
를 사용할 수 있는 것이다.
또한 delivery
함수를 보면 await asyncio.sleep()
이 있다. 이는 asyncio 안에 있는 sleep 함수를 사용했기 때문에 어웨이터블 객체이고, 따라서 await를 사용할 수 있다.
다음으로 Task는 코루틴을 동시에 예약하는데 사용한다.
코루틴이 asyncio.create_task()
와 같은 함수를 사용하여 task로 싸일 때, 코루틴은 곧 실행되도록 자동으로 예약이 된다.
Future는 비동기 연산의 최종 결과를 나타내는 특별한 row-level 어웨이터블 객체이다.
다음으로 간단한 asyncio
메소드 들을 알아보겠다
-
asyncio.run(coroutine)
- 코루틴을 실행하고 결과를 반환한다.
-
asyncio.create_task(coroutine)
- 코루틴을 Task로 감싸고 실행을 예약한다. Task 객체를 반환한다.
async def delivery(name, mealtime): print(f"{name}에게 배달 완료!") await asyncio.sleep(mealtime) print(f"{name} 식사 완료, {mealtime}시간 소요...") print(f"{name} 그릇 수거 완료") return mealtime async def main(): await delivery('A',1) await delivery('B',2) await delivery('C',3) if __name__ == "__main__": start = time.time() asyncio.run(main()) end = time.time() print(end - start) # A에게 배달 완료! # A 식사 완료, 1시간 소요... # A 그릇 수거 완료 # B에게 배달 완료! # B 식사 완료, 2시간 소요... # B 그릇 수거 완료 # C에게 배달 완료! # C 식사 완료, 3시간 소요... # C 그릇 수거 완료 # 6.017804145812988
위 코드를 보면 asyncio를 써서 더 빠르게 처리되어야 할텐데 시간이 동일하게 걸렸다.
왜냐하면 코루틴 함수를 여러 개를 한 번에 실행해야 하는데 await 코루틴함수
이런식으로 쓰면 그냥 코루틴을 호출하는 것이지 다음 태스크를 실행하도록 예약을 하는 것이 아니기 때문이다.
여러 코루틴을 동시에 실행하기 위해선 create_task()
로 등록해야한다. 아래 코드처럼 변경하여 실행하면 시간이 3초로 줄어들 것이다.
async def main(): task1 = asyncio.create_task(delivery('A',1)) task2 = asyncio.create_task(delivery('B',2)) task3 = asyncio.create_task(delivery('C',3)) await task1 await task2 await task3
asyncio.sleep(delay, result=None, loop=None)
- delay 초 동안 블로킹 한다.
- result가 제공되면, 코루틴이 완료될 때 호출자에게 반환된다.
- sleep()은 현재 태스크를 일시 중단해서 다른 태스크를 실행할 수 있게 한다.
asyncio.gather(*aws, loop=None, return_exceptions=False)
- aws 시퀀스에 있는 어웨이터블 객체를 동시에 실행합니다.
- aws에 있는 어웨이터블이 코루틴이면 자동으로 task로 예약됩니다.
- 즉, 동시에 task를 실행한다!
- 모든 어웨이터블이 성공적으로 완료되면, 결과는 반환된 값들이 합쳐진 리스트입니다. 결괏값의 순서는 aws에 있는 어웨이터블의 순서와 일치합니다.
import time import asyncio async def delivery(name, mealtime): print(f"{name}에게 배달 완료!") await asyncio.sleep(mealtime) print(f"{name} 식사 완료, {mealtime}시간 소요...") print(f"{name} 그릇 수거 완료") return mealtime async def main(): result = await asyncio.gather( # 동시에 delivery 함수들이 실행된다.(동시성) delivery("A", 1), delivery("B", 2), delivery("C", 3), ) print(result) # result에 delivery 함수의 return 값이 인자로 들어간다. if __name__ == "__main__": start = time.time() asyncio.run(main()) end = time.time() print(end - start)
💻 출력
A에게 배달 완료! B에게 배달 완료! C에게 배달 완료! A 식사 완료, 1시간 소요... A 그릇 수거 완료 B 식사 완료, 2시간 소요... B 그릇 수거 완료 C 식사 완료, 3시간 소요... C 그릇 수거 완료 [1, 2, 3] # 3.021336793899536
출력을 보면 어웨이터블 객체 delivery가 동시에 실행되는 것을 알 수 있고, 이를 동시성이라 한다.
📌파이썬 코루틴 활용
웹 데이터를 가져오는 실습을 하며 코루틴을 활용해보자.
requests
패키지를 사용하여 웹의 데이터를 가지고 올 수 있다.
def io_bound_func():
result = requets.get("https://naver.com")
return result
위 함수는 한번 요청을 보내고 응답을 받으면 끊기게 되는데, 이는 세션을 사용하면 해결할 수 있다.
세션이란 서버와 클라이언트간 연결을 유지시켜주는 역할을 한다. 사용자가 웹 브라우저를 통해 웹서버에 접속한 시점으로부터 웹 브라우저를 종료하여 연결을 끝내는 시점까지, 같은 사용자로부터 오는 일련의 요청을 하나의 상태로 보고 그 상태를 일정하게 유지한다.
즉, 방문자가 웹 서버에 접속해있는 상태를 하나의 단위로 보고 그것을 세션이라고 한다.
( 출처: https://cheershennah.tistory.com/135 [Today I Learned. @cheers_hena 치얼스헤나] )
requests
패키지에서 세션에 대한 글이 있다. requests.org (어렵다.. 좀 더 공부를 해야한다..)
일단 코드를 보며 이해해보자.
# pip install requests
import requests
def main():
url = "https://naver.com"
session = requests.Session() # 세션을 열고
session.get(url) # url에 대한 정보를 가져오고
session.close() # 세션을 꼭 닫아줘야 한다!!!
위 코드는 다음과 같이 with
구문을 이용하면 간략하게 사용할 수 있다.
def main():
with requests.Session() as session:
session.get(url)
음.. 일단 어떻게 세션을 열고 닫고, url의 정보를 가져오는지 알았다.
다음 코드를 보자.
def fetcher(session, url):
with session.get(url) as response:
return response.text
def main():
url = "https://naver.com"
with requests.Session() as session:
result = fetcher(session, url)
print(result)
위 코드를 보면 main() 함수에서 세션을 열고 fetcher 함수을 호출한다. fetcher 함수는 session과 url을 입력으로 받아 url의 text정보를 반환하고, main 함수는 그것을 출력한다.
위 코드를 실행시켜 보면 네이버의 첫 페이지에 대한 html 내용이 나오는 것을 확인할 수 있다.
이런 방식으로 웹에서 데이터를 가져올 수 있다.
이제 우리는 간단하게 웹의 데이터를 가져오는 방법을 알았다.
이제 여러개의 url에 요청을 보내고 응답을 받는 시간을 재는 코드를 보자.
import requests
import time
def fetcher(session, url):
with session.get(url) as response:
return response.text
def main():
urls = ["https://naver.com", "https://google.com",
"https://instagram.com"] * 10
with requests.Session() as session:
result = [fetcher(session, url) for url in urls]
print(result)
if __name__ == "__main__":
start = time.time()
main()
end = time.time()
print(end - start) # 12
위 코드는 네이버, 구글, 인스타그램을 각각 10번씩 순회하며 데이터를 가져오는데, 그 시간은 약 12초가 걸린다.(시간은 조금씩 다르게 나올 수 있다.)
이제 이 코드를 코루틴을 사용하여 비동기 코드로 바꿔서 동시성프로그래밍으로 바꿔보자.
requests 패키지는 동기적인 코드가 있는 패키지이고, 우리는 비동기적 코드가 필요하다. 바로 aiohttp
패키지이다. 더욱 자세한 내용은 aiohttp 사이트 에 가서 확인해보면 좋겠다.
필자는 pip install aiohttp==3.7.3
버전을 다운받았다.
import aiohttp
import time
import asyncio
async def fetcher(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = ["https://naver.com", "https://google.com",
"https://instagram.com"] * 10
async with aiohttp.ClientSession() as session:
result = await asyncio.gather(*[fetcher(session, url) for url in urls]) # 언패킹
print(result)
if __name__ == "__main__":
start = time.time()
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
asyncio.run(main()) # 코루틴 함수 실행
end = time.time()
print(end - start) # 4.8
마찬가지로 우리가 필요한 패키지들을 임포트하자.
aiohttp 사이트에서 확인해보면 알 수 있듯이 세션을 열때 aiohttp.ClientSession()
을 사용한다.
이후에 fetcher 함수와 main 함수 앞에 async
키워드를 붙여 코루틴으로 바꿔준다. 또한, 기존의 response.text
가 response.text()
로 어웨이터블 객체가 되기 때문에 앞에 await 키워드를 붙여주자. 여기서 await가 사용이 되었기 때문에 with 문 또한 비동기함수가 되어야 한다. 그러므로 with 앞에 async
키워드를 붙여준다.
동시성프로그래밍을 하기 위해서 await asyncio.gather()
메소드를 사용해야 하고, 그러면 with구문 앞에 또 async 키워드를 붙여줘야 한다.
참고
Author And Source
이 문제에 관하여(파이썬 코루틴과 비동기 함수), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@wodnr0710/파이썬-코루틴과-비동기-함수저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)