[Python] asyncio

·2023년 4월 19일
0

동시 프로그래밍 패러다임 변화

전통적으로 동시 프로그래밍은 여러 개의 쓰레드를 활용해 이루어졌다.
쓰레드를 이용해 직접 코딩을 해보면 thread safe한 프로그램을 작성하는것은 쉬운 일이 아니다.
싱글 코어 프로세서에서 이런 프로그램을 돌리면, 기대했던 동시 처리에 따른 성능 향상은 미미하거나 심지어 성능 저하도 된다.

이런 이유로 하나의 쓰레드로 동시 처리를 하는 비동기 프로그래밍이 더욱 주목 받는다.

비동기 프로그래밍

웹 서버와 같은 애플리케이션을 생각해보면 CPU 연산 시간 대비 DB나 API와 연동 과정에서 발생하는 대기 시간이 훨씬 길다는 걸 알 수 있다. 비동기 프로그래밍은 이런 대기 시간을 낭비하지 않고 그 시간에 CPU가 다른 처리를 할 수 있도록 하는데 이를 흔히 non-blocking하다고 한다.

JS 같이 애초에 비동기 방식으로 설계된 언어에서는 익숙한 개념이지만 Python같이 기본적으로 동기 방식으로 동작하는 언어에서는 이 개념이 생소하게 느껴질 수 있다.
하지만 Python 3.4에서 asyncio 가 표준 라이브러리로 추가되고 Python 3.5에서 async/await 키워드가 문법으로 채택 되며, Python도 이제 언어 자체적으로 비동기 프로그래밍이 가능해졌다.

핵심 문법

def 키워드로 선언하는 모든 함수는 Python에서 기본적으로 동기 방식으로 동작한다고 생각하면 된다.

예를 들어 다음과 같이 선언된 함수는 동기함수다.

def do_sync():
	pass

기존 def 키워드 앞에 async 키워드까지 붙이면 이 함수는 비동기 처리되며, 이러한 비동기 함수를 Python에서는 coroutine 라고 부른다.

async def do_async():
	pass

이런 비동기 함수는 일반 동기 함수가 호출하듯이 호출하면 coroutine 객체가 리턴된다

do_async() # <coroutine object do_async at 0x1038de710>

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

async def main_async():
	await do_async()

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

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

Python 3.7 이상에서는 다음과 같이 한 줄로 간단히 비동기 함수 호출 할 수도 있다.
asyncio.run(main_async())

실습

사용자 관리 애플리케이션을 흉내내는 실습 코드 작성하며 동기 처리하는 코드, 비동기 처리 하는 코드 비교

다음과 같은 가정

  • 애플리케이션을 사용자 데이터를 직접 보관하지 않고 외부 API를 호출해서 가져온다.
  • 외부 API는 1명의 사용자 데이터를 조회하는데 1초가 걸리고, 한 번에 여러 사용자의 데이터를 조회할 수 없다.
  • 각각 3명,2명,1명의 사용자 정보를 조회하는 요청 3개가 동시에 애플리케이션으로 들어온다.

동기 프로그래밍

먼저 사용자 데이터의 조회를 전통적인 동기 방식으로 처리해주는 find_users_sync 함수를 작성한다. 의도적으로 1초의 지연 시간을 발생시키기 위해서 time.sleep 함수를 사용한다.

import time

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

그 다음, 애플리케이션에 들어온 3개 요청 동기 처리하는 process_sync 함수를 작성

def process_sync():
	start = time.time()
    find_users_sync(3)
    find_users_syne(2)
    find_users_sync(1)
    end = time.time()
    print(f'>>> 동기 처리 총 소요 시간: {end - start}`)
    
if __name__ == '__main__':
	process_sync()

이 함수 호출해보면 find_users_sync 함수 총 6초 동안 3번 순차적으로 실행됨을 알 수 있다.

3명 중 1번 째 사용자 조회 중 ...
3명 중 2번 째 사용자 조회 중 ...
3명 중 3번 째 사용자 조회 중 ...
> 총 3 명 사용자 동기 조회 완료!
2명 중 1번 째 사용자 조회 중 ...
2명 중 2번 째 사용자 조회 중 ...
> 총 2 명 사용자 동기 조회 완료!
1명 중 1번 째 사용자 조회 중 ...
> 총 1 명 사용자 동기 조회 완료!
>>> 동기 처리 총 소요 시간: 6.020448923110962

만약 싱글 thread의 웹 서버가 이러한 방식으로 동작한다면 실제 사용자는 얼마나 오랫동안 지연을 경험하게 된다. 동기 처리에서는 첫 번째 함수의 실행이 끝나야 두 번째 함수가 실행되고, 마찬가지로 두 번째 함수가 끝나야 세 번째 함수가 실행된다. 즉, 첫 번째 요처이 처리되는데는 3초, 두 번째 요청은 5초 (3+2), 세 번째 요청은 6초 (3+2+1)가 걸릴 것이다.

비동기 프로그래밍

위에서 동기 처리되도록 작성된 코드를 파이썬의 async/await 키워드를 사용해서 한 번 비동기 처리될 수 있도록 개선해보도록 한다. 기존의 함수 선언에 async 키워드를 붙여서 일반 동기 함수가 아닌 비동기 함수(coroutine)로 변경했으며, time.sleep 함수 대신 asyncio.sleep 함수 사용해 1초의 지연 발생

time.sleep 함수는 기다리는 동안 CPU를 그냥 놀리는 반면, asyncio.sleep함수는 CPU가 놀지 않고 다른 처리를 할 수 있도록 해준다. 여기서 주의할 점은 asyncio.sleep자체도 비동기 함수이기 때문에 호출할 때 반드시 await키워드를 붙여야 한다는 것이다.

import time
import asyncio

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

Python asyncio라이브러리를 사용해 위에서 작성한 함수를 비동기로 실행해본다.
먼저 이벤트 루프가 3개의 함수 호출을 알아서 스케줄해 비동기로 호출할 수 있도록
asyncio.wait 함수의 배열 인자로 3개의 함수 리턴값, 즉 coroutine 객체를 넘겨주도록 수정한다. 그리고 이렇게 수정된 process_async 비동기 함수를 호출할 때도, 함수의 리턴값인 coroutine 객체를 asyncio.run 함수에 넘겨준다.

async def process_async():
	start = time.time()
    await asyncio.wait([
    	find_users_async(3),
        find_users_async(2),
        find_users_async(1),
    ])
    end = time.time()
    print(f'>>> 비동기 처리 총 소요 시간: {end - start}`)
    
if __name__ == '__main__':
	asyncio.run(process_async())

비동기 처리되도록 재작성된 코드 실행해보면 호출 순서와 무방하게 실행 시간이 짧을 수록 먼저 처리되는 것을 알 수 있다. 게다가 총 소요시간도 6초에서 3초로 100% 단축되었음을 알 수 있다.

실제 사용자 관점에서 생각해보면 3초가 걸리는 요청을 기다리지 않고, 1초가 걸리는 요청은 1초만에 응답이 오고, 2초가 걸리는 요청은 2초 만에 응답이 올테니 매우 이상적이다.

1명 중 1번 째 사용자 조회 중 ...
2명 중 1번 째 사용자 조회 중 ...
3명 중 1번 째 사용자 조회 중 ...
> 총 1 명 사용자 비동기 조회 완료!
2명 중 2번 째 사용자 조회 중 ...
3명 중 2번 째 사용자 조회 중 ...
> 총 2 명 사용자 비동기 조회 완료!
3명 중 3번 째 사용자 조회 중 ...
> 총 3 명 사용자 비동기 조회 완료!
>>> 비동기 처리 총 소요 시간: 3.0041661262512207

기본적으로 비동기 처리는 정확히 실행 순서가 보장되지 않기 때문에, 실행순서가 PC마다 다를 수 있다. 비록 동일한 실행 순서 보장받지 못하더라도, 여기서 중요한 점은 CPU 놀리지 않고 불필요한 지연없이 3개의 요청이 실행되어야 한다는 것이다.

출처 https://www.daleseo.com/python-asyncio/

0개의 댓글