python

python asyncio gather vs wait

seokhyun2 2023. 5. 21. 14:26

개요

파이썬에서 fastapi가 점점 많이 사용되고 있다보니 자연스럽게 async를 사용해서 코딩을 하게 되는 것 같습니다.

async에서 제일 착각하기 쉬운게, async 함수를 연속으로 사용하면 비동기로 동시에 처리될 것이라고 생각할 수 있는데요.

아래 코드를 실행해보면, 총 2초가 걸리는 것을 확인할 수 있습니다.

import time
import asyncio


async def method1():
    await asyncio.sleep(1)


async def method2():
    await asyncio.sleep(1)


async def main():
    await method1()
    await method2()


if __name__ == "__main__":
    start = time.time()
    asyncio.run(main())
    end = time.time()
    print(end - start)

main에서 method1을 먼저 기다리고 끝난 뒤에, method2를 시작하도록 구현되어 있어서, 2초가 걸리는 것인데요.

 

우리는 method1, method2가 동시에 시작해서 1초만에 수행하길 바라는 경우가 있습니다.

예를 들면 동시에 2개의 DB에서 데이터를 조회하거나 또는 동시에 여러 개의 API를 호출하여 결과를 합치는 경우가 있을텐데요.

이럴 때는, 어떻게 해야하는 지 알아보겠습니다.

 

gather()

우선 gather 함수를 사용할 수 있습니다.

바로 코드를 보면 아래와 같습니다.

import time
import asyncio


async def method1():
    await asyncio.sleep(1)


async def method2():
    await asyncio.sleep(1)


async def main():
    await asyncio.gather(method1(), method2())


if __name__ == "__main__":
    start = time.time()
    asyncio.run(main())
    end = time.time()
    print(end - start)

await asyncio.gather(method1(), method2()) 와 같이 활용하면 method1, method2를 동시에 실행하게 됩니다.

gather() 함수의 경우에는, parameter로 여러 개의 코루틴을 넘겨주면 되어서 매우 직관적이고 쉽게 사용할 수 있습니다.

 

결과를 반환할 때도, 아래와 같이 코드를 구현해서 확인해보면 results가 [1,2]로 입력한 method의 순서대로 반환 값을 리스트에 넣어서 보여주기 때문에 쉽게 사용할 수 있습니다.

import time
import asyncio


async def method1():
    await asyncio.sleep(1)
    return 1


async def method2():
    await asyncio.sleep(1)
    return 2


async def main():
    results = await asyncio.gather(method1(), method2())
    print(results)


if __name__ == "__main__":
    start = time.time()
    asyncio.run(main())
    end = time.time()
    print(end - start)

 

gather 함수는 매우 직관적이고 쉬운 인터페이스로 쉽게 사용할 수 있지만, 모든 코루틴이 끝날때까지 기다려야만 한다는 단점이 있습니다.

wait()

위의 gather 함수의 단점을 보완할 수 있는 것이 바로 wait입니다.

wait함수는 return_when 파라미터는 FIRST_COMPLETED, FIRST_EXCEPTION, ALL_COMPLETED 3가지 중 하나를 입력받아서,조건이 충족되는 경우에 끝나도록 되어 있습니다.

FIRST_COMPLETED는 어떤 함수든 하나라도 끝이나면 그 순간 반환하고, ALL_COMPLETED는 모든 함수가 끝나야 반환합니다.

FIRST_EXCEPTION은 처음 Exception이 발생하는 순간 반환하는데, Exception이 전혀 발생하지 않는다면 ALL_COMPLETED와 동일하게 모든 함수가 끝나면 반환됩니다.

import time
import asyncio


async def method1():
    await asyncio.sleep(1)


async def method2():
    await asyncio.sleep(2)


async def main():
    done, pending = await asyncio.wait([method1(), method2()], return_when=asyncio.FIRST_COMPLETED)
    print(done)
    print(pending)


if __name__ == "__main__":
    start = time.time()
    asyncio.run(main())
    end = time.time()
    print(end - start)

method1은 1초, method2는 2초를 기다리도록 구성했지만 위와 같이 코드를 짜서 돌려보면 1초만에 코드가 끝이 나고 done, pending에 각각 하나씩 무언가가 들어가 있다는 것을 확인할 수 있을텐데요.

wait 함수는 2개의 리스트를 반환하는데, done은 끝난 함수의 리스트 pending은 아직 돌고 있는 함수의 리스트입니다.

done에 들어있는 함수들의 결과에 따라서, pending에 들어있는 함수들은 cancel도 할 수 있고, wait를 한번 더 써서 끝날때까지 또 기다리도록 코드를 구현할 수 있습니다.

wait를 활용하면 확실히 유연하게 사용할 수 있다는 것을 볼 수 있는데요.

추가적으로 wait는 timeout 파라미터도 입력으로 넣을 수 있습니다.

 

gather(), wait() 차이

위에서 조금씩 다 설명을 했지만, gather()와 wait()의 차이를 한번 더 정리해보겠습니다.

gather()는 여러 개의 함수를 파라미터로 입력받고, 모든 함수가 끝나는 순간에 입력받은 함수의 순서대로 각 함수의 반환값을 리스트로 반환합니다.

매우 쉽고 직관적인 인터페이스를 제공하는데 반면 wait()는 함수의 리스트를 입력으로 받으면서 추가로 return_when, timeout 파라미터를 가지고 유연하게 활용할 수 있습니다.