본문 바로가기
Python/Data Engineering

Python asyncio를 활용한 비동기 프로그래밍

by JONGSKY 2023. 8. 25.
728x90
SMALL

1. 글을 쓰게 된 이유

 

서비스 프로그램을 만들면서 asynico를 사용해서 코딩을 해야하는데

생각보다 비동기를 사용하면 굉장히 좋은 서비스를 만들 수 있고

속도 문제도 해결할 수 있다는 사실을 알게되었다.

 

2. 파이썬에서 비동기 프로그래밍

 

- 자바스크립트와 같이 애초에 비동기 방식으로 동작하도록 설계된 언어는 익숙한 개념이다

- 파이썬과 같이 동기 방식으로 동작하는 언어에서는 생소하지만

- 파이썬 3.4에서 asyncio 모듈이 표준 라이브러리로 추가되면서

- 파이썬 3.5에서 async / await 키워드가 문법으로 채택이 되면서

- 파이썬도 외부 라이브러리 없이 프로그래밍이 가능해졌다.

 

3. 핵심 문법

 

- def(동기 방식) / async def(비동기 방식)

 

아래와 같은 비동기 방식으로 만들어진 함수를 코루틴(coroutine)이라고 부름

async def test():
	return "test"

 

- async def(비동기  함수)는 일반적으로 async 로 선언된 다른 비동기 함수 내에서 await 키워드를 붙여서 호출해야함.

async def main_test():
    await test()

 

 

- async 로 선언되지 않은 일반 동기 함수 내에서는 비동기 함수를 호출하려면 asyncio 라이브러리의 이벤트 루프를 이용해야함.

 

loop = asyncio.get_event_loop()
loop.run_until_complete(main_test())
loop.close()

※ 파이썬 3.7 이상에서는 다음과 같이 한줄로 간단히 비동기 함수를 호출 가능하다.

asyncio.run(main_async())

 

- 비동기 프로그래밍 asyncio.sleep()은 CPU가 놀지 않고 다른 처리를 할 수 있도록 해준다. 여기도 await 키워드는 붙여야 한다.

   (time.sleep()은 함수를 기다리는 동안 CPU를 그냥 놔둠)

 

import asyncio

async def number_test(n):
    for i in range(1, n + 1):
        print(f'{n}명 중 {i}번 째 사용자 조회 중')
        await asyncio.sleep(1)
    print(f'> 총 {n}명 사용자 비동기 조회 완료')

 

- asyncio.gather() 함수는 비동기 함수들을 병렬로 실행하고 그 결과를 모아주는 역할, 인자로 넘어오는 비동기 함수들을 동시에 실행하고 각 비동기 함수가 변환하는 결과들을 모아서 리스트로 반환

단, 비동기 처리는 정확히 실행 순서가 보장되지 않기 때문에 코드를 작성할 때 꼭 고려해야 한다.

 

import asyncio
import time

async def number_test(n):
    for i in range(1, n + 1):
        print(f'{n}명 중 {i}번 째 사용자 조회 중')
        await asyncio.sleep(1)
    print(f'> 총 {n}명 사용자 비동기 조회 완료')

async def process_async():
    start = time.time()
    await asyncio.gather(
        number_test(3),
        number_test(2),
        number_test(1),
    )
    end = time.time()
    print(f'>>> 비동기 처리 총 소요 시간: {end - start}')

if __name__ == '__main__':
    asyncio.run(process_async())

 

- 코루틴과 테스크의 차이

 

1. 코루틴을 사용했을 때

import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    print(f"started at {time.strftime('%X')}")

    await say_after(1, 'hello')
    await say_after(2, 'world')

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

 

2. 테스크를 사용했을 때

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())

 

Q. 코루틴을 사용했을 때는 3초, 테스크를 사용했을 때는 2초가 걸리는 이유는 무엇일까?

A. async def로 선언 된 함수를 호출하면, 코드가 실행 되지 않고 코루틴 객체를 리턴하기만 할 뿐이다.

    하지만, create_task는 반환된 객체를 가지고 비동기 작업 객체인 테스크를 만들고 실행한다.

간단하게 말해서, 코루틴은 비동기 작업의 단위 / create_task()는 비동기 작업을 관리하고 실행하기 위한 도구다.

코루틴에서는 async def안에 작성한 코드가 실행되는 시점은

await say_after(1, "hello")

입니다. say_after(1, "hello") 호출로 코루틴 객체가 생성되고, await에 의해 함수 내 작성한 코드가 동기적으로 실행되면서 끝날 때 까지 대기합니다. 반면 테스크는,

    task1 = asyncio.create_task(
            say_after(1, 'hello'))

이 부분에서 이미 코드를 비동기로 실행했습니다.

그리고 await task 는 이미 실행한 코드를 대기할 뿐입니다. 따라서 task1과 task2의 await 순서를 변경해도 같은 결과를 얻습니다.

 

4. 출처

 

Coroutines and Tasks

This section outlines high-level asyncio APIs to work with coroutines and Tasks. Coroutines, Awaitables, Creating Tasks, Task Cancellation, Task Groups, Sleeping, Running Tasks Concurrently, Shield...

docs.python.org

 

728x90
LIST