본문 바로가기
프로그래밍 언어/Python

[Python] 비동기 프로그래밍

by 토마토베이컨수프 2021. 12. 7.

비동기 프로그래밍동기적 프로그래밍의 반대말인데요, 그럼 동기적 프로그래밍은 뭘까요?

일반적으로 우리가 작성한 코드는 프로세스가 진행됨에 따라 코드상 작성된 작업들을 순차적으로 처리하게 되는데, 하나의 작업을 시작하면 그 작업이 끝날 때 까지 다른 작업을 시작하지 않습니다. 이를 동기적 프로그래밍이라 합니다.

 

그러나 비동기 프로그래밍은 응답을 기다려야 일이 발생했을 때 기다리지 않고 바로 다음 작업에 착수합니다. 따라서 데이터를 요청하고 응답을 기다리는 스타일의 코드에서 큰 성능을 보일 수 있습니다.

 

비동기 프로그래밍에 대한 설명을 시작하기 전, 용어들에 대한 정리를 먼저 확실하게 해두겠습니다.

  • 이벤트 루프: 이벤트 루프는 여러 작업들을 돌아가며 하나씩 실행시키는 메인 작업 관리자의 역할을 합니다. 만약 한 작업을 수행하는 중 불필요한 대기 시간이 발생하였을 시, 이벤트 루프는 다른 작업을 실행시켜 전반적으로 코드가 비동기적으로 돌아가게 합니다.
  • 코루틴: 이벤트 루프가 실행하는 작업들을 이루는 함수입니다. 이 함수는 비동기적 성향을 띄고 있어 작동 중 응답이 지연되면 이벤트 루프에게 통제권을 넘겨주고, 응답이 완료되면 전에 멈추었을 때의 상태를 유지한 채로 남은 작업을 완료합니다.

 

파이썬에서는 asyncio라는 모듈을 이용해 비동기 프로그래밍을 구현할 수 있습니다. asyncio 모듈은 파이썬 버전에 따라 문법이라든지 사용 트렌드가 많이 다르기 때문에 이 글을 작성하는 시점에서 가장 최신 버전인 파이썬 3.10.0의 공식문서를 참조하여 비동기 사용 방법을 소개해드리겠습니다.

 


코루틴 생성 및 실행

import asyncio

async def main():
    await asyncio.sleep(1)
    print('hello')

asyncio.run(main())	# hello

함수 선언 앞에 async를 붙이면 코루틴을 만들 수 있습니다. 

그리고 asyncio.run() 함수를 통해서 이벤트 루프를 생성해 코루틴을 작동시킬 수 있습니다. 모든 작업이 끝나면 이 이벤트 루프는 자동으로 종료됩니다. 만약 동일 스레드 내에 다른 이벤트 루프가 이미 돌아가고 있다면, 이 함수는 호출할 수 없습니다.

 

asyncio.sleep()는 코드를 몇 초간 대기시키는 함수로, time.sleep()과 역할이 같습니다. 코루틴이 아닌 함수는 비동기에서 사용할 수 없기 때문에 asyncio.sleep() 함수를 사용해준 것입니다.

 


태스크

async 키워드로 코루틴이 된 함수는 일반적인 방법으로는 실행시킬 수 없습니다. 위 코드처럼 asyncio.run()로 실행시키거나 asyncio.create_task() 함수를 이용해 태스크를 만든 후 await 키워드를 통해 실행시켜야 합니다. 이렇게 await를 통해서 작동시킬 수 있는 객체(코루틴, 태스크 등)를 awaitable 객체라고 합니다.

async def is_prime(value):
    divisor = value // 2
    await asyncio.sleep(10 - value)
    for i in range(2, divisor + 1):
        if value % i == 0:
            print(f"{value} is not a prime")
            return
    print(f"{value} is a prime")

# 잘못된 코루틴 호출
is_prime(5)	

# 올바른 코루틴 호출
async def main():
    task1 = asyncio.create_task(is_prime(5))
    task2 = asyncio.create_task(is_prime(7))
    await task1
    await task2
    
asyncio.run(main())

 

asyncio.create_task() 함수를 통해 태스크가 생성된 순간, 그 태스크는 이벤트 루프에 의해 미래에 작동될 것이라 스케쥴링되고, await같은 키워드를 통해 호출되는 순간 비동기 루프 속에 참여하게 되는 것입니다.

 

그렇다면 위의 코드가 실행되는데 소요되는 시간은 총 몇 초일까요? 파라미터로 넘겨주는 value값에 따라 태스크 당 (10-value)초가 걸리니, task1은 5초, task2는 3초가 걸립니다. 동기적 프로그래밍이라면 총 8초가 걸리겠지만, task1 에서 asyncio.sleep()를 만나게 되는 순간 이벤트 루프는 task2로 넘어가 새로운 태스크를 수행하게 되고, task2가 먼저 작업이 끝나니 task2의 결과를 먼저 출력할 것입니다. 총 소요 시간은 task1이 결과를 출력하는데 걸리는 시간인 5초가 될 것입니다.

# 7 is a prime
# 5 is a prime

 


복수의 태스크에 대한 비동기적 처리

위에서는 태스크를 하나씩 만들어서 await 키워드를 통해 작동시켰습니다. asyncio.gather() 함수를 사용하면 여러개의 태스크를 만들어 한번에 스케쥴링 할 수 있습니다.

async def factorial(name, number):
    result = 1
    for i in range(2, number + 1):
        print(f"Task [{name}]: Currently on process {result} X {i}")
        await asyncio.sleep(1)
        result *= i
    print(f"Task [{name}]: Result = {result}")
    
async def main():
    await asyncio.gather(
        factorial("A", 3),  # 2초
        factorial("B", 4),  # 3초
        factorial("C", 5),  # 4초
    )
    
asyncio.run(main())

 

이처럼 asyncio.gather()의 인자로 코루틴을 여러개 호출해주기만 하면 됩니다. 이 코드는 Task A, B, C 순서대로 결과값이 출력되고 총 소요시간은 4초가 될 것입니다.

# Result
"""
Task [A]: Currently on process 1 X 2
Task [B]: Currently on process 1 X 2
Task [C]: Currently on process 1 X 2
Task [A]: Currently on process 2 X 3
Task [B]: Currently on process 2 X 3
Task [C]: Currently on process 2 X 3
Task [A]: Result = 6
Task [B]: Currently on process 6 X 4
Task [C]: Currently on process 6 X 4
Task [B]: Result = 24
Task [C]: Currently on process 24 X 5
Task [C]: Result = 120
"""

 

위 코드를 다음과 같은 방법으로 작성할 수도 있습니다.

async def main():
    task_name = ["A", "B", "C"]
    
    tasks = [factorial(i, j) for i, j in zip(task_name, range(3, 6)]
    await asyncio.gather(*tasks)
    
asyncio.run(main())

 


시간 제한

asyncio.wait_for() 함수를 사용하면 코루틴이 작동될 시간을 제한할 수 있습니다. 함수 내 timeout 인자를 사용하여 제한시간을 설정할 수 있고, 시간 초과시 TimeoutError가 발생하며 코루틴이 종료됩니다.

async def five_seconds():
    await asyncio.sleep(5)
    print("Coroutine ends")
    
async def main():
    task_deprecate = asyncio.wait_for(five_seconds(), timeout=3)
    try:
        await task_deprecate 
    except asyncio.TimeoutError:
        print("Timeout has occured")
        
asyncio.run(main())

 

위 코드는 5초가 소요되는 five_seconds()라는 비동기 함수를 실행하는 코드입니다. 그러나 timeout=3으로 3초의 시간 제한을 주었기 때문에, "Coroutine ends"라는 텍스트는 출력되지 않고 코드 시작 3초만에 "Timeout has occured"라는 메세지를 띄우고 프로그램은 종료될 것입니다.

 


이터러블 코루틴

asyncio.as_completed() 함수를 이용해 다수의 코루틴들을 이터레이터로 만든 후 루프문에 넣어 각각의 코루틴에 접근할 수 있습니다. 바로 코드를 한번 봅시다.

async def return_value(value):
    await asyncio.sleep(5 - value)
    return value
    
async def main():
    aws = [return_value(x) for x in range(1, 4)]
    for coro in asyncio.as_completed(aws):
        result = await coro
        print(f"{result} has returned")
        
asycnio.run(main())

 

이 코드는 return_value(1), return_value(2), return_value(3) 코루틴을 연달아 실행시키는 코드랑 동일합니다, 결과는 아래와 같이 나오고 총 소요시간은 4초입니다.

3 has returned
2 has returned
1 has returned

참고로, asyncio.as_completed() 함수도 timeout 인자를 가지고 있어 (디폴트값 None), 모든 코루틴이 완료되기 전 timeout이 생기면 TimeoutError가 발생하게 됩니다.

 


참고 자료

'프로그래밍 언어 > Python' 카테고리의 다른 글

[Python] 디버깅과 로그  (0) 2021.12.16
[Python] 데코레이터  (0) 2021.11.21
[Python] 클래스 구조  (0) 2021.11.16
[Python] 제너레이터  (0) 2021.11.11
[Python] 유용한 데이터 구조  (0) 2021.11.07