[TIL] Python | 비동기 프로그래밍 (Async)

은경·2022년 4월 5일
0
post-thumbnail
post-custom-banner

📌

동시 프로그래밍은 여러 개의 쓰레드를 활용하여 이루어졌었다. 하지만 thread safe한 프로그램을 작성하는 것은 어렵다. 특히 싱글 코어 프로세서에서 이런 프로그램을 돌렸을때, 성능이 향상되지 않거나 떨어지는 경우도 존재한다.

따라서 하나의 쓰레드로 동시 처리를 하는 비동기 프로그래밍(asynchronous programming)에 대해서 알아보자.

  • 동기 함수 (sync function)
    함수 호출 -> 함수의 처음부터 진행 -> 끝 또는 return문을 만나면 종료.
    리턴 했다는 것은 함수가 실행 완료 되었다는 것을 보장해줌.

  • 비동기 함수
    함수를 호출 -> 실행이 완료 되지 않더라도 호출자에게 return, 제어권을 넘기고 자기 혼자 백그라운드로 작업을 계속 진행 -> 작업이 완료 되면 호출자에게 작업이 완료 되었음을 통보
    리턴 되고 제어권이 호출자에게 넘어와도, 작업이 완료 되었음을 보장하지 않음.


비동기 프로그래밍 개념


웹 서버와 같은 애플리케이션의 경우 CPU 연산 시간 보다 DB나 API와 연동 과정에서 발생하는 대기 시간이 훨씬 길다.
비동기 프로그래밍은 이러한 대기 시간을 낭비하지 않고 그 시간에 CPU가 다른 처리를 할 수 있도록 함 -> non-blocking : I/O 작업이 진행되는 동안 유저 프로세스의 작업을 중단시키지 않는 방식

동기란 빨래를 시작하고 종료가 되면 설거지를 시작하고 완료가 되면 TV를 보는 것처럼 한 번에 하나의 작업을 하는 것이고, 비동기는 빨래를 시작시키고 설거지를 하면서 TV를 보는 것과 같이 여러 작업을 동시에 하는 것과 같은 행동을 하는 것이다.

JavaScript는 비동기 방식으로 동작하도록 설계된 언어인 반면, Python은 동기 방식으로 동작하도록 설계된 언어 이기 때문에 생소한 개념일 수 있다.
하지만 Python 3.4 버전부터 asyncio 라이브러리가 표준으로 채택되고 Python 3.5 버전부터 async/await 키워드가 추가되면서, Python에서도 비동기 프로그래밍을 더욱더 쉽게 할 수 있게 되었다.


asyncio란?


Python 3.5부터 지원하는 asyncio는 비동기 프로그래밍을 위한 모듈. 파이썬에서는 GIL때문에 비동기 프로그래밍이 동기 프로그래밍보다 느릴 수도 있다.

asyncio이벤트 루프코루틴을 기반으로 동작하며 데이터를 요청하고 응답을 기다리는 I/O bound한 작업에서 효율적. 코루틴 기반이므로 멀티 스레드(CPU bound, 연산 중심)방식과 비교하여 문맥교환에 따른 비용이 다소 적게 들어간다.

일반적으로 연산이 많이 필요한 로직은 CPU bound
ex)데이터 마이닝, 이미지 프로세싱, 암호화폐 마이닝
로컬 파일 시스템 혹은 네트워크 통신이 많은 로직은 I/O bound라고 한다.


🟩 Event Loop

  • 이벤트 루프는 작업들을 반복 하면서(루프를 돌면서) 하나씩 실행시킴
  • 실행된 작업이 특정한 데이터를 요청 후 응답을 기다려야 하면, 이 작업은 다시 이벤트 루프에 통제권을 넘겨줌
  • 통제권을 받은 이벤트루프는 다음 작업을 실행
  • 이전 요청작업의 응답을 받으면 순서대로 멈췄던 부분부터 다시 통제권을 가지고 작업을 마무리함

🟨 Coroutine

  • 코루틴은 파이썬에서 async를 통해 생성된 비동기 함수 객체를 뜻함
  • 응답이 지연되는 부분에서 이벤트 루프에 통제권을 줄 수 있음
  • 요청이 완료되면 멈춘부분부터 기존의 상태를 유지한 채 남은 작업을 완료 할 수 있는 함수
  • 코루틴이 아닌 일반적인 함수는 Subroutine(서브루틴)이라고 함

🟪 Asyncio

  • async/await 문법을 사용해서 cpu작업과 I/O를 병렬적으로 처리하여 비동기 프로그래밍을 할 수 있도록 하는 파이썬 라이브러리

  • async

    • 비동기 함수를 생성하는 키워드
    • def앞에 async 키워드를 붙여서 비동기 함수를 생성.
    • async 키워드를 통해 생성된 비동기 함수를 코루틴 이라고 함
  • await

    • 비동기 함수를 호출하는 키워드
    • 코루틴을 일반 함수와 같이 호출하면 코루틴 객체를 반환함
    • await키워드를 통해서 호출해야 비동기 함수를 실행 가능
# 사용 예시

async def async_func():
	pass
    
async def run_async_func():
	await async_func()

🟦 사용 함수 예시

  • asyncio.get_event_loop()

    • 현재의 이벤트 루프를 반환하는 함수
    • async로 선언되지 않은 일반 함수에서 비동기 함수를 호출하기 위해서는 이벤트 루프를 사용해야 함
  • loop.run_until_complete(future)

    • future가 완료할 때 까지 이벤트 루프를 실행시킴
    • 실행한 결과(future)의 결과를 반환
  • asyncio.Future

    • 비동기 연산의 최종 결과를 나타내는 객체
      async def async_func():
           pass
      loop = asyncio.get_event_loop()       # 이벤트 루프 정의
      loop.run_until_complete(async_func()) # 비동기 함수 async_func를 호출
      loop.close()                          # 이벤트 루프 종료 
  • asyncio.gather()

    • 여러 함수를 동시에 호출할 때 사용
      loop.run_until_complete(asyncio.gather(coroutine_1(), coroutine_2()))
  • asyncio.run()

    • 간단하게 비동기 함수를 호출하는 함수
    • Python 3.7 부터 사용 가능
  • asyncio.wait()

    • 여러개의 함수를 리스트에 담아서 호출 가능
      async def start_coroutine():
      	await asyncio.wait([
        	     coroutine_1(),
               coroutine_2()
          )]
          
      asyncio.run(start_coroutine())
  • asyncio.sleep()

    • 비동기 함수로써 시작하자 마자 바로 리턴하지만, 백그라운드로 1초간 대기 후 시간이 만료 되면 만료를 통보한다.
  • loop.run_in_executor(executor, func, *args)

    • 동기 함수를 비동기로 동작 할 수 있도록 하는 함수
    • 지정된 executor에서 func가 호출되도록 등록한다. executor 가 None 이면 기본 executor가 사용 된다.
  • import asyncio
  • async def로 함수 선언
  • await로 비동기 함수의 요청 결과를 기다림
  • asyncio.gather() 함수로 여러 비동기 함수를 동시에 호출
  • asyncio.run() 함수로 비동기 함수 실행

asyncio를 사용한 비동기 프로그래밍 예제


1️⃣ sleep 함수를 이용한 소요 시간 비교

  • 동기 방식 : task1 실행 후 task2 실행 -> 총 8초의 시간이 걸림
def sync_task_1():
    print('sync_task_1 : 시작')
    print('sync_task_1 : 5초 대기')
    time.sleep(5)
    print('sync_task_1 : 종료')

def sync_task_2():
    print('sync_task_2 : 시작')
    print('sync_task_2 : 3초 대기')
    time.sleep(3)
    print('sync_task_2 : 종료')

start_time = time.time()
sync_task_1()
sync_task_2()
end_time = time.time()
print (end_time-start_time) 
# 8.009934186935425 초 
  • 비동기 방식 : task1을 실행하는 동안 task2를 실행함 -> 총 5초의 시간이 걸림
async def async_task_1():
    print('async_task_1 : 시작')
    print('async_task_1 : 5초 대기')
    await asyncio.sleep(5)
    print('async_task_1 : 종료')

async def async_task_2():
    print('async_task_2 : 시작')
    print('async_task_2 : 3초 대기')
    await asyncio.sleep(3)
    print('async_task_2 : 종료')

async def main():
    start_time = time.time()
    # 여러 코루틴을 동시에 실행하기 위해선 create_task()를 사용 
    # await async_task_1()
    # await async_task_2() <- 이런식으로 나열하면 코루틴을 호출하는 것이지 다음 태스크를 실행하도록 예약하는 행동이 아님 
    task1  = asyncio.create_task(async_task_1()) 
    task2  = asyncio.create_task(async_task_2())
    await task1
    await task2
    end_time = time.time()
    print(f"소요시간 : {end_time - start_time}")
    
asyncio.run(main())
# 소요 시간 : 5.003206968307495 초

2️⃣ 웹 페이지를 불러올 때의 소요 시간 비교

  • 동기 방식
def download_page(url):
    req = requests.get(url)
    html = req.text
    print('다운로드 완료 :', url, ',페이지 크기 (', len(html),')')

def main():
    download_page('https://velog.io/@fore0919/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-K%EB%B2%88%EC%A7%B8-%EC%88%98-%EC%A0%95%EB%A0%AC-Python')
    download_page('https://velog.io/@fore0919/TIL-WEB-TCPIP-%EA%B0%9C%EB%85%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0')
    download_page('https://velog.io/@fore0919/TIL-Python-sleep-%ED%95%A8%EC%88%98')
    download_page('https://velog.io/@fore0919/TIL-Middleware')
    download_page('https://velog.io/@fore0919/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC-%EC%99%84%EC%A0%84-%ED%83%90%EC%83%89-Python')
    download_page('https://velog.io/@fore0919/DB-Data-Type%EC%9E%90%EB%A3%8C%ED%98%95-MySQL')
    download_page('https://velog.io/@fore0919/DB-SQL-Constaint-%EC%A0%9C%EC%95%BD-%EC%A1%B0%EA%B1%B4')

print (f"시작 시간{time.strftime('%X')}")
start_time = time.time()
main()
finish_time = time.time()
print(f"종료 시간{time.strftime('%X')}, 총 소요 시간 : {finish_time - start_time}초")
# 총 소요 시간 : 3.3226277828216553초 

  • 비동기 방식
async def download_page2(url):
    loop = asyncio.get_event_loop() # 이벤트 루프 객체 얻기
    req = await loop.run_in_executor(None, requests.get, url) # 동기함수인 requests.get을 비동기로 호출 
    html = req.text
    print('다운로드 완료 :', url, ',페이지 크기 (', len(html),')')

async def main():
    await asyncio.gather(
        download_page2('https://velog.io/@fore0919/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-K%EB%B2%88%EC%A7%B8-%EC%88%98-%EC%A0%95%EB%A0%AC-Python'),
        download_page2('https://velog.io/@fore0919/TIL-WEB-TCPIP-%EA%B0%9C%EB%85%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0'),
        download_page2('https://velog.io/@fore0919/TIL-Python-sleep-%ED%95%A8%EC%88%98'),
        download_page2('https://velog.io/@fore0919/TIL-Middleware'),
        download_page2('https://velog.io/@fore0919/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC-%EC%99%84%EC%A0%84-%ED%83%90%EC%83%89-Python'),
        download_page2('https://velog.io/@fore0919/DB-Data-Type%EC%9E%90%EB%A3%8C%ED%98%95-MySQL'),
        download_page2('https://velog.io/@fore0919/DB-SQL-Constaint-%EC%A0%9C%EC%95%BD-%EC%A1%B0%EA%B1%B4')
    )

print (f"시작 시간{time.strftime('%X')}")
start_time = time.time()
asyncio.run(main())
finish_time = time.time()
print(f"종료 시간{time.strftime('%X')}, 총 소요 시간 : {finish_time - start_time}초")
# 총 소요 시간 : 0.7937102317810059초

참고 자료 (Reference)


https://docs.python.org/ko/3.8/library/asyncio.html
https://kukuta.tistory.com/345
https://jammdev.tistory.com/37
https://brownbears.tistory.com/540
https://dojang.io/mod/page/view.php?id=2469

profile
Python 서버 개발자
post-custom-banner

0개의 댓글