본문 바로가기
Coding

Python 3의 비동기 프로그래밍에 대해 알아보자

by Jakegyllenhaal 2022. 4. 19.
반응형

Python 3의 비동기 프로그래밍에 대해 알아보자

 

비동기 Python 코드 실행

( Pixabay에서 Boskampi에 대한 이미지 크레딧)

Python은 배우기 가장 쉬운 언어 중 하나로 간주됩니다. 반면에 비동기 코드에 대한 Python 접근 방식은 상당히 혼란스러울 수 있습니다. 이 기사에서는 접근하기 쉽게 만드는 비동기 Python 코드의 주요 개념과 예제를 살펴봅니다.

특히 다음을 제거해야 합니다.

  1. 비동기 프로그래밍의 핵심 어휘
  2. 비동기식 접근 방식이 타당한 경우
  3. Python 3의 비동기 코드 기본 사항
  4. 추가 조사에 유용한 리소스

시작하자!

비동기 프로그램은 병렬로 작업을 실행합니다. 메인 프로세스를 차단하지 않고. 그것은 한 입 가득하지만 이것이 의미하는 모든 것입니다. 비동기 코드는 프로그램이 다른 작업을 수행할 수 있을 때를 기다리는 데 불필요하게 시간을 소비하지 않도록 하는 방법입니다.

이전에 비동기 프로그래밍에 대해 읽은 적이 있다면 아마도 체스 예제를 수십 번 들었을 것입니다(한 번에 모든 게임을 한 번에 한 게임씩 토너먼트를 하는 마스터 체스 플레이어). 이것은 개념을 설명하는 데 고전적으로 도움이 되지만, 음식을 요리하는 것은 염두에 두어야 할 매운 세부 사항과 함께 보다 관련성 높은 은유를 제공합니다.

동시 요리

이런 아침식사를 얼마나 자주 해봤니?

1단계: 계란 요리(채식 친구들을 위한 토스트)

2단계: 베이컨(오트밀) 요리

3단계: 차가운 계란/토스트와 뜨거운 베이컨/오트밀 먹기

바라건대, 대답은 "말 그대로 절대"입니다. 다음 요리로 넘어가기 전에 한 번에 하나의 요리를 요리하는 것은 아마도 꽤 거친 음식을 만들 것입니다(그리고 완전히 비효율적입니다). 이것은 우리가 부르는: synchronous cooking. 정기적으로 이 작업을 수행하는 친구가 있으면 즉시 도와주세요.

비동기식 요리

적당한 양의 음식을 만들 때 한 번에 한 접시를 준비하고 싶은 경우는 드뭅니다. 대신에 오트밀과 토스트를 만든다면 커피를 넣고 물을 끓이기 시작하고 오트밀과 빵을 꺼냅니다. 물이 끓으면 오트밀을 시작하고 오트밀이 준비되기 몇 분 전에 토스터기에 토스트를 팝니다.

이제 모든 것이 준비되면 뜨거운 커피, 토스트 및 오트밀을 거의 동시에 먹을 수 있기를 바랍니다. 이것은 우리가 부르는: asynchronous cooking.

모든 것을 동시에 요리한다고 해서 각 요리의 요리 시간이 줄어들지는 않습니다. 여전히 토스트가 황금빛 갈색이 되고 커피가 스며들도록 해야 하며 오트밀은... 오트밀이 준비되면 무엇이든 해야 합니다. 토스트를 만드는 데는 동기식과 동일한 시간이 비동기식으로 걸립니다.

그러나 각 항목이 완료될 때까지 기다리는 시간을 낭비하는 대신, 작업은 요리 진행 단계에 따라 수행됩니다.. 즉, 여러 작업이 가능한 한 빨리 시작되고 귀중한 시간이 효율적으로 사용됩니다.

고려 사항

비동기 접근 방식의 또 다른 중요한 기능은 순서는 다른 작업으로 넘어갈 때보다 덜 중요합니다.. 예를 들어 3코스 식사를 요리하는 경우 첫 번째 코스가 두 번째 코스보다 먼저 나오고 세 번째 코스가 먼저 나옵니다. 이 시나리오에서는 이러한 요리를 동시에 요리해야 할 수도 있습니다.

비동기 접근 방식이 올바른 경우에도 새 작업으로 이동할 때를 아는 것은 유용하게 만드는 데 매우 중요합니다. 예를 들어, 끓는 물의 경우 스토브를 켜고 빵을 꺼낸 다음 냄비를 꺼내는 등의 작업을 앞뒤로 바꿀 수 있지만 실제로 가치가 있습니까?

우리는 기다릴 것이 별로 없을 때 작업 사이를 뛰어다녔습니다. 끓는 물에서 시간이 오래 걸리는 것은 스토브에서 물을 데워야 하는 부분입니다. 물을 끓일 비동기식 설정 덜 효율적일 수도 있습니다 작업 사이를 앞뒤로 움직여야 하기 때문에(이를 실행 오버헤드예를 들어 빵을 사러 스토브에서 식료품 저장실로 걸어가다가 냄비가 스토브에 가까울 때 식료품 저장실에서 냄비 보관실로 이동).

요컨대, 비동기는 모든 사용 사례를 위한 것이 아니며 기존 동기 코드를 마술처럼 더 빠르게 만들지 않습니다. 또한 설계가 간단하지 않으며 효율성보다 순서가 더 중요한 위치에 대해 사전에 충분히 생각해야 합니다.

그 확장된 비유를 제외하고 비동기 파이썬 코드가 실제로 어떻게 생겼는지 봅시다!

웹에서 Python에서 비동기 프로그램을 작성하는 다양한 방법에 대한 자료를 많이 볼 수 있습니다(예: 콜백, 생성기 등 — 이에 대한 전체 검토를 위해 이 연습을 권장합니다). 그러나 Python의 최신 비동기 코드는 일반적으로 다음을 사용합니다. async 그리고 await.

싱크대와 무게?

async 그리고 await 비동기 프로그램을 작성하는 데 사용되는 Python 3의 키워드입니다. 그만큼 async/await 구문은 다음과 같습니다.

async def get_stuff_async() -> Dict:
results = await some_long_operation()
return results["key"]

이것은 동기 버전과 크게 다르지 않습니다.

def get_stuff_sync() -> Dict:
results = some_long_operation()
return results["key"]

유일한 텍스트 차이점은 async 그리고 await. 그래서, 무엇을 할 async 그리고 await 실제로합니까?

async 우리의 함수가 비동기 작업이라고 선언합니다.await 다음까지 이 작업을 일시 중지할 수 있음을 Python에 알립니다. some_long_operation 완성 됐습니다.

따라서 이 두 호출의 기능적 차이점은 다음과 같습니다.

  1. get_stuff_sync우리는 부른다 some_long_operation해당 호출이 돌아올 때까지 기다립니다. results, 다음 결과의 중요한 하위 집합을 반환합니다. 우리가 기다리는 동안 results다른 작업을 실행할 수 없습니다. 블로킹 전화.
  2. get_stuff_async우리 일정 some_long_operation그 다음에 수율 제어 메인 스레드로 돌아갑니다. 한번 some_long_operation 보고 results, get_stuff_async 실행을 재개하고 중요한 하위 집합을 반환합니다. results. 우리가 기다리는 동안 results메인 프로세스는 다른 작업을 자유롭게 실행할 수 있습니다. 논블로킹 전화.

이것은 추상적인 예이지만 이미 이 비동기 접근 방식의 이점(및 결함) 중 일부를 볼 수 있습니다. 의 구현 get_stuff_async 리소스 사용에 대한 보다 효율적인 접근 방식을 제공하는 동시에 get_stuff_sync 시퀀싱과 더 간단한 구현에 대한 확실성을 제공합니다.

그러나 비동기 함수와 메서드를 사용하는 것은 이 예제보다 조금 더 복잡합니다.

이전 예에서 몇 가지 중요한 새 어휘를 보았습니다.

  • 일정
  • 생산하다
  • 블로킹
  • 논블로킹
  • 메인 스레드

Python에서 비동기 코드를 실행하는 방법을 배우면서 이 모든 것을 더 쉽게 설명할 수 있습니다.

동기화 프로그램에서 다음을 수행할 수 있습니다.

if __name__ == "__main__":
results = get_stuff_sync()
print(results) # returns “The Emperor’s Wayfinder is in the Imperial Vault”

그리고 결과를 콘솔에 출력할 것입니다.

비동기 코드로 이 작업을 수행하면 다소 다른 메시지가 표시됩니다.

if __name__ == "__main__":
results = get_stuff_async()
print(results)# returns
# bonus!! RuntimeWarning: coroutine 'get_stuff_async' was never awaited

이것이 우리에게 말해주는 것은 get_stuff_async 반환 coroutine 우리의 중요한 결과 대신에 목적을 달성합니다. 그것은 또한 우리에게 그 이유에 대한 단서를 제공합니다. 우리는 함수 자체를 결코 기다리지 않았습니다.

그래서, 우리는 단지 넣어야 합니다 await 함수 호출 앞, 맞죠? 불행히도, 그것은 그렇게 간단하지 않습니다. await 내부에서만 사용할 수 있습니다. async 기능이나 방법. 최상위 '대기' 대신에 로직을 다음으로 예약해야 합니다. 이벤트 루프 사용 asyncio.

이벤트 루프

Python에서 비동기 작업의 핵심은 이벤트 루프입니다. "the" 이벤트 루프라고 하면 일정 수준의 중력이 부여됩니다. 맞죠? 사실 이벤트 루프는 모든 종류의 프로그램에서 사용되며 특별하거나 마법 같은 것이 아닙니다.

웹 서버를 선택하십시오. 요청을 기다린 다음 요청을 수신하면 이를 이벤트로 처리하고 해당 이벤트를 응답과 일치시킵니다(예: 이 기사의 URL로 이동하면 매체 백엔드는 "새 이벤트: 해당 브라우저 요구했다 super-genius-article 우리는 돌아가야 한다 super-genius-article.html"). 이벤트 루프입니다.

"The" 이벤트 루프는 비동기 작업을 예약할 수 있는 Python 내장 이벤트 루프를 나타냅니다(다중 스레딩 및 하위 프로세스에서도 작동함). 이것에 대해 정말로 알아야 할 것은 프로세스가 완료될 때를 알 수 있도록 일정을 예약한 작업을 이벤트와 일치시킨다는 것입니다.

그것을 사용하기 위해 우리는 표준 라이브러리를 사용합니다 asyncio 모듈, 다음과 같이:

import asyncioif __name__ == "__main__": # asyncio.run is like top-level `await`
results = asyncio.run(get_stuff_async())
print(results)# returns “The Emperor’s Wayfinder is in the Imperial Vault”

대부분의 시나리오에서 이것은 이벤트 루프에서 필요한 전부입니다. 저수준 라이브러리 또는 서버 코드를 작성할 때 이벤트 루프에 직접 액세스하려는 일부 고급 사용 사례가 있지만 지금은 이것으로 충분합니다.

따라서 우리의 작은 샘플 스크립트는 다음과 같습니다.

import asyncioasync def some_long_operation():
return {"key": "The Emperor's Wayfinder is in the Imperial Vault"}async def get_stuff_async():
results = await some_long_operation()
return results["key"]if __name__ == "__main__":
results = asyncio.run(get_stuff_async())
print(results)

이것은 확실히 작동하지만 이 논리에는 비동기 동작을 요구하거나 이점을 얻는 것이 없으므로 비동기가 실제로 유익한 더 강력한 예를 살펴보겠습니다.

웹의 다른 위치와 데이터를 주고받는 것은 비동기 프로그래밍의 사용 사례입니다. 느린 원격 API에서 응답을 기다리는 것은 재미가 없습니다. 다른 데이터를 기다리는 동안 다른 중요한 작업을 실행하면 프로그램의 효율성을 높일 수 있습니다. 이제 그 예를 작성해 보겠습니다.

빠르고 느린 서버

이 예를 설명하기 위해 느린 원격 API를 직접 작성합니다.

import time
import uvicorn
from fastapi import FastAPIapp = FastAPI() # the irony, amirite?
@app.get("/{sleep}")
def slow(sleep: int):
time.sleep(sleep)
return {"time_elapsed": sleep}
if __name__ == "__main__":
uvicorn.run(app) # uvicorn is a server built with uvloop, an asynchronous event loop!

이것은 경로 매개변수를 받는 하나의 끝점이 있는 간단한 서버입니다. sleep, 해당 시간 동안 휴면한 다음 JSON 응답에서 해당 숫자를 반환합니다. (품질 API 작성에 대해 자세히 알고 싶으십니까? 새 기사가 곧 나올 예정입니다!)

말할 필요도 없이 이것은 우리가 기다리고 있을 수 있는 느린 작업을 시뮬레이션할 수 있게 해줍니다.

빠른 비동기 스크립트

이제 클라이언트 코드에 대해 임의의 숫자를 선택하고 임의의 시간 동안 기다립니다.

import asyncio
import aiohttpfrom datetime import datetime
from random import randrange
async def get_time(session: aiohttp.ClientSession, url: str):
async with session.get(url) as resp: # async context manager!
result = await resp.json()
print(result)
return result["time_elapsed"]
async def main(base_url: str):
session = aiohttp.ClientSession(base_url)
# select 10 random numbers between 0, 10
numbers = [randrange(0, 10) for i in range(10)]
# await responses from each request
await asyncio.gather(*[
get_time(session, url)
for url in [f"/{i}" for i in numbers]
])
await session.close()if __name__ == "__main__":
start_time = datetime.now()
asyncio.run(main("http://localhost:8000"))
print(start_time - datetime.now())

이 스크립트를 실행하면 다음과 같은 출력이 나타납니다.

[7, 2, 6, 4, 0, 9, 4, 2, 5, 5] # times we requested the API to wait
{'time_elapsed': 0} # API response JSON for waiting X seconds
{'time_elapsed': 2}
{'time_elapsed': 2}
{'time_elapsed': 4}
{'time_elapsed': 4}
{'time_elapsed': 5}
{'time_elapsed': 5}
{'time_elapsed': 6}
{'time_elapsed': 7}
{'time_elapsed': 9}
0:00:09.020562 # time it took the program to run

그다지 빠르지 않은 동기화 스크립트

반복성을 위해 동일한 요청 시간을 사용하는 클라이언트 코드의 동기 버전은 다음과 같습니다.

import requests
from datetime import datetime
def get_time(url: str):
resp = requests.get(url)
result = resp.json()
print(result)
return result["time_elapsed"]
def main(base_url: str):
numbers = [7, 2, 6, 4, 0, 9, 4, 2, 5, 5]
print(numbers) for num in numbers:
get_time(base_url + f"/{num}")
if __name__ == "__main__":
start_time = datetime.now()
main("http://localhost:8000")
print(datetime.now() - start_time)

결과:

# returns:[7, 2, 6, 4, 0, 9, 4, 2, 5, 5] # same numbers
{'time_elapsed': 7}
{'time_elapsed': 2}
{'time_elapsed': 6}
{'time_elapsed': 4}
{'time_elapsed': 0}
{'time_elapsed': 9}
{'time_elapsed': 4}
{'time_elapsed': 2}
{'time_elapsed': 5}
{'time_elapsed': 5}
0:00:44.099638 # 5x slower

즉시 숫자 목록이 정렬되지 않았지만 비동기 클라이언트 코드를 실행할 때 원격 API의 출력이 정렬되었음을 알 수 있습니다. 왜냐하면 우리는 결과를 받을 때 결과를 출력하고 더 긴 대기가 더 짧은 대기보다 자연스럽게 늦게 반환되기 때문입니다.

둘째, 가장 긴 대기 시간인 9초가 넘는 시간 동안 10개의 전화를 걸고 있습니다. ~이다 9초. 그만큼 동기 실행 시간은 API에서 요청한 모든 시간의 합계이므로 다음 호출로 이동하기 전에 각 호출이 완료될 때까지 대기하므로 44초(또는 ~5배 느림)입니다. 그만큼 비동기 실행 시간은 다음과 같습니다. 우리가 요청한 가장 긴 대기 시간 (최대 9초 이 클라이언트 코드에서) 훨씬 더 효율적입니다.

비동기 및 동기화 호출이 모두 동일한 시간(44초) 동안 API 대기를 요청하지만 비동기 프로그래밍을 사용하면 전체 프로그램이 훨씬 더 빨라집니다.

마지막으로 이 클라이언트 코드 샘플은 asyncio 그리고 aiohttp:

  1. 비동기 컨텍스트 관리자(async with 구문)은 일반 with 문이 사용되지만 비동기 코드에서
  2. 그만큼 aiohttp.ClientSession object는 클라이언트 측 비동기 네트워크 요청을 작성하기 위한 API입니다. 자세한 내용은 다음 문서에서 확인하세요. aiohttp
  3. 그만큼 asyncio.gather 호출은 비동기 함수 그룹을 실행하고 그 결과를 목록으로 반환하는 정말 편리한 방법입니다. 여러 위치에서 데이터가 필요하지만 단일 함수를 기다리기를 원하지 않을 때 이를 사용하여 유용한 API 호출을 만드는 것을 상상할 수 있습니다. 요구

이 기사가 Python에서 비동기 프로그래밍을 사용할 때 사용할 수 있는 약간의 정보를 제공했기를 바랍니다. 포괄적인 것은 아니며 병렬 프로그래밍 패러다임과 관련하여 추구해야 할 방대한 지식이 있지만 이제 시작입니다.

앞으로 나아갈 때 기억해야 할 주요 사항:

  1. 비동기가 항상 슬램 덩크는 아닙니다. 많은 사용 사례에서 모든 프로그램이 데이터가 돌아올 때까지 기다리지 않아도 되므로 동기화 실행이 비동기 프로그래밍보다 간단하고 빠릅니다.
  2. 비동기를 사용하려면 프로그램의 순서와 언제 어떤 데이터가 필요한지에 대한 디자인 사고가 필요하지만 동기 코드는 모든 호출에서 즉시 반환되는 데이터가 있는 것으로 간주하는 경향이 있습니다.
  3. 비동기 코드를 작성하는 방법에는 여러 가지가 있지만 거의 항상 async/await — 다른 것이 필요한지 확실하지 않은 경우 원하는 async/await 통사론

지금은 여기까지입니다! 좋은 밤 되시고 비동기 프로그래밍 여정에 행운을 빕니다.

이것은 이 기사를 만드는 데 도움이 된 모든 것을 합한 것은 아니지만, 상관없이 볼 수 있는 좋은 것입니다.

  1. 비동기: https://docs.python.org/3/library/asyncio.html
  2. uvloop: https://uvloop.readthedocs.io/
  3. FastAPI: https://fastapi.tiangolo.com/
  4. 굉장한 Asyncio: https://github.com/timofurrer/awesome-asyncio
반응형

댓글