python async def(비동기 함수)

gunny·2024년 2월 23일
0

Python

목록 보기
2/29

1. 글을 쓰게 된 배경

주니어긴 하지만 초년따리도 아니고 항상 동기/비동기 함수에 대해서 들어본 바는 있지만 비동기 함수를 구현했을 때 제대로 이해하지 않고 그냥 비동기가 필요하니까~ 하고 코드 복붙 했던 나를 까면서 이번에 제대로 동기, 비동기 함수를 이해하고 구현한 코드를 이해하고 그리고 앞으로 제대로 구현해보고자 기록한다.

2. Synchronous vs Asynchronous(동기 vs 비동기)

먼저 동기와 비동기

동기(synchronous) 방식

  • 요청과 결과가 동시에 일어나는 방식으로 요청을 보낸 후 응답을 받아야 다음 동작이 진행
  • 예를 들어 특정 기능을 구동하는데 시간이 5분 소요된다고 하면 이 기능이 구동하는 5분 동안 다른 기능을 동작시키지 못함.
  • 장점 : 설계 간단하고 직관적
  • 단점 : 요청에 대한 결과가 반환될 때까지 대기

비동기(Asynchronous) 방식

  • 요청과 결과가 동시에 일어나지 않는 방식으로 요청과 결과가 동시에 일어나지 않음. 요청을 보내고 요청에 대한 응답을 기다리지 않고 다른 것을 수행할 수 있고, 다른 요청을 보낼 수 있음
  • 예를 들어 위의 특정 기능을 구동하는데 시간이 5분 소요된다고 할때, 그 시간 동안 다른 프로그램을 수행할 수 있음
  • 장점 : 자원을 효율적으로 사용 가능
  • 단점 : 동기 방식보다 설계 복잡

즉, 동기는 하나의 기능을 구현하고 구동하고 끝날 때까지 기다려야 하는 것이고
비동기는 하나의 기능을 구현하고 해당 기능이 구동되는 동안 다른 기능이 구동될 수 있다는 것이다.

3. python에서의 def / async def

그렇다면 python에서 동기와 비동기 함수를 구현해서 적용한다면?
먼저 그냥 평소에 우리가 구현하는 def 함수는 동기적인 함수이다.
그리고 여기에 'async'를 붙여주면 비동기 함수로 구현할 수 있다.

def

  • 동기적인 함수를 정의할 때 사용
  • 함수가 호출되면 코드 실행이 해당 함수 내부로 진입하고, 함수 내부의 모든 작업이 완료 될때까지 코드 실행이 멈춘다.
  • 어떤 객체나 값도 반환할 수 있다.

async def

  • 비동기 함수를 정의할 때 사용
  • 비동기 함수는 일반적으로 비동기적으로 실행되고, 함수가 실행되는 동안 다른 작업들이 동시에 실행될 수 있음
  • 코루틴이므로, 일반적으로 await를 통해 비동기적으로 실행할 수 있는 객체를 반반환다. 그러나, 필수적이지 않고 만약 함수가 아무것도 반환하지 않으면 기본적으로 None을 반환한다.

4. async def (비동기 함수) 를 위한 베이스 (asyncio, 이벤트 루프, 코루틴)

  • python 3.5 부터 asyncio로 비동기 프로그래밍을 할 수 있다.
  • python에서는 Global Interpreter Lock(GIL) 때문에 비동기 프로그래밍이 동기프로그래밍보다 늦을 수도 있다.
    해당 부분은 다른 게시물을 파서 따로 정리해야 겠다.

비동기 함수를 구현하기 위해서는 asyncio를 import 해서 사용한다.

asyncio

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

python asyncio 공식문서

https://docs.python.org/ko/3.8/library/asyncio.html

여기서 또 이벤트 루프와 코루틴이라는 용어가 등장하는데

이벤트 루프

  • 작업들을 반복문을 돌면서 하나씩 실행 시킨다.
    실행한 작업이 데이터를 요청하고 응답을 기다리면 다른 작업에게 evnet loop에 대한 권한을 넘긴다. 권한을 받은 event loop는 다음 작업을 실행하고, 응답을 받은 순서대로 대기하던 작업부터 다시 권한을 가져와 작업을 마무리 한다.

코루틴(Coroutie)

  • 'async def' 함수는 코루틴으로 간주되는데, 이는 실행이 중지되고 다른 코루틴이 실행되는 경우에도 나중에 다시 시작할 수 있는 함수를 의미한다.

python 코루틴 공식 문서

https://docs.python.org/ko/3/library/asyncio-task.html

비동기/대기(async/await) 구문으로 선언된 코루틴은 비동기 응용 프로그램을 작성하는 데 선호되는 방법이다.

코루틴으로 태스크를 만들었으면 asyncio.get_evnet_loop 함수를 통해서 이벤트 루프를 정의하고 run_until_compltet로 실행한다.

asyncio를 사용하기 위해서는 함수 앞에 async를 붙이면 코루틴으로 만들 수 있다. I/O가 발생하거나 권한을 다른 작업에 넘기고 싶으면 해당 로직앞에 await을 붙인다. 이때 await 뒤에 오는 코드는 코루틴으로 작성된 코드여야 하는데,
awiat 뒤에 time.sleep과 같이 사용되면 스레드가 중단되므로 의도한 되로 동작하지 않아 asyncio.sleep을 사용해야 한다.
코루틴으로 만들어진 모듈이 아니라면 await을 붙여도 소용 없다.

비동기(asynchronous) 함수와 'await'

  • 'await' 키워드는 비동기 함수 내에서만 사용할 수 있다.
  • async def 는 'await' 키워드를 사용해서 비동기 작업의 완료를 기다린다.
    다른 비동기 함수를 호출하거나 비동기적으로 작동하는 객체의 메서드를 호출할 때 사용한다.

5. async def 구현 예시

asyncio를 사용해서 몇 예제로 살펴보자면,
python과 jupyter notebook에서 asyncio를 실행하는 방법이 조금 다르다.
일단 python은 docs 기준에 따라서 asyncio.run()으로 실행하는데
jupyter notebook은 await 으로 실행한다.

'asyncio' 모듈을 사용해서 비동기적인 작업을 수행하는 샘플 코드이다. (python 예제)

async def main():
    print('hello')
    await asyncio.sleep(1)
    print('world')
    
asyncio.run(main())
  • 첫 번째 줄은 async def로 비동기를 정의하는 문법을 사용하고 main을 함수의 이름으로 둔다.
    해당 함수의 이름은 비동기 작업의 시작점이다.
  • 두 번째 줄은 비동기 함수의 첫 번째 작업으로 'hello'를 출력하는 일반적인 동기적인 코드이다.
  • 세 번째 줄은 awiat asyncio.sleep(1) 으로 await 키워드를 사용해서 비동기적으로 실행되는 함수나 코루틴이 완료 될 때 까지 대기한다. 여기서는 1초간 대기하는 asyncio.sleep(1)를 호출 했는데, 'asyncio.sleep(1)'은 1초 동안 이벤트 루프를 블록하지 않고 다른 작업이 수행될 수 있도록 한다.
  • 네 번째 줄은 print('world')로 asyncio.sleep(1)이 완료되면 즉, 'hello'를 출력 후 1초후에 world를 출력한다.
  • 마지막 줄 asyncio.run(main())은 이벤트 루프를 생성하고 비동기 함수 main()을 실행한다.

jupyter의 경우에는 asyncio.run의 경우 에러가 나는데
await main()으로 실행한다.

사용 방법은 위와 같고, 위의 예제는 태스크가 1개라서 동기 프로그래밍과 같다.


2개의 태스크가 있는 함수를 동기와 비동기로 실행해보면
[동기]

[비동기]

로 똑같이 비동기로 했지만 5초가 걸리는 것을 볼 수 있다.
그 이유는 코루틴 함수는 여러개를 '한 번에 실행' 해야 하는데,
await 코루틴 함수()로 나열하면 코루틴을 호출하고 끝인 것이다.
다음 태스크를 실행하도록 예약하는 'create_task()'를 사용해야 한다.


위와 같은 바식으로 asyncio.create_task()로 각각 함수를 등록하는 방법도 있고, 등록해야 하는 함수가 많으면 asyncio.gather()를 사용할 수 있다.

등록한 코루틴 함수가 모두 성공하면 등록된 순서대로 결과값이 리스트로 반환된다.
asyncio.gather() 함수의 세 번째 파라미터는 return_exceptions으로 기본값은 False인데, 코루틴 함수 중 1개라도 에러가 나면 즉시 중단하고 에러를 발생시킨다.
해당 인자를 True로 변경한다면 에러가 난 코루틴 함수가 실행되지 않고 결과 리스트에 에러 정보가 반환된다.


그 외에 await 표현식에서 사용할 수 있는 객체는 awaitable 객체라고 하는데,
해당 객체는 코루틴(coroutine), 태스크(task), 퓨처(future)가 있다.

6. awaitable 객체

(1) 코루틴(coroutine)

  • 코루틴 이라는 용어는 코루틴 함수와 코루틴 객체 두 의미를 내포한다.
  • 코루틴 함수는 async def 로 정의된 함수이고, 코루틴 객체는 코루틴 함수를 호출하고 반환되는 객체이다.

코루틴 함수를 await을 붙이지 않고 호출하면 호출되지 않는다
코루틴이 생성 됐지만 await 하지 않아 아무것도 실행되지 않는 것이다.

그래서 await을 붙여줘야 한다.

(2) 태스크(task)

  • 태스크는 코루틴을 동시에 예약하는데 사용되는데,
    asyncio.create_task()와 같은 함수를 사용해 코루틴이 실행되도록 자동으로 예약한다.

(3) 퓨처(future)

  • 퓨처는 비동기 연산의 최종 결과를 나타내는 저수준 awaitable 객체이다.
    asyncio와 future를 사용하는 좋은 예는 loop.runin_executor() 인데,
    아래의 예제는 멀티 스레드나 멀티 프로세스를 활용한 방식으로,
    코루틴이 아니기 때문에 문맥교환 비용이 발생한다.
    장점은 일반 함수를 수정하지 않고 비동기로 동작하게 만들 수 있는데
    내가 참고한 티스토리에서도 ㅍ처 객체만 사용해서 스레드나 프로세스를 사용하는 방식보다 asyncio
    fueture를 사용하는데 이점을 모르겠다고 작성되어 있다.

퓨처까지는 좀 더 공부해야 알 것 같다.
아직 비동기 async 구현 이해를 해야합니다.

참고한 사이트

일단 비동기 함수 관련해서 asyncio는 대충 감을 잡았으니,
작성되어 있는 코드 리뷰를 하면서 이해하고, 또 비동기 코드를 작성해봐야겠다.

profile
꿈꾸는 것도 개발처럼 깊게

0개의 댓글