Language

[ python ] future / task object

궁금한게 많은 개발자 2023. 4. 16. 16:49

Future Object

퓨처 객체는 어떠한 작업의 실행 상태 및 결과를 저장하는 객체입니다. 실행 상태란 해당 작업이 진행 중인지(PENDING), 취소되었는지(CANCELLED), 종료되었는지(FINISHED)를 나타냅니다. 작업의 완료라 함은 CANCELLED 또는 FINISHED 상태를 가리킵니다. 

그리고 실행 결과라 함은 해당 작업의 결과 값 혹은 그 작업을 진행하면서 발생한 예외 객체를 말합니다. 예외가 발생한 경우에도 상태는 FINISHED가 됨을 주의해야 합니다.

중요한 점으로는 작업의 실행 상태 및 결과를 나타낼 뿐, 작업의 실행을 개시하지 않는다는 점입니다.

 

퓨처 객체의 중요한 메소드 중하나는 add_done_callback()입니다. 이 메소드를 호출하면 해당 퓨처 객체가 완료될 떄 호출할 함수를 등록할 수 있습니다. event loop의 동작 원리를 설명할 때 중요한 내용입니다.  

 

 

 

Task Object

테스크는 퓨처를 상속하는 클래스입니다. 즉, 테스크는 기본적으로 퓨처 객체의 기능을 전부 가지고 있으므로 작업의 실행 상태나 결과를 저장합니다. 그러나 중요한 차이점으로 테스크 객체는 어떠한 작업의 실행을 개시하는 역할도 수행합니다.

이를 위해 필요한 것이 바로 coroutine 객체입니다. 실제로 task 객체는 생성될 때 coroutine객체를 넘겨받아 _coro 필드에 저장합니다. 결국 테스크 객체는 코루틴 객체를 갖고 있는 특별한 종류의 퓨처 객체입니다.

 

task를 생성하려면 asyncio.run(), asyncio.create_task()함수를 호출할 때 coroutine객체를 넘겨주면서 해당 coroutine을 가지는 task를 생성합니다. task객체는 생성되는 즉시 현재 thread에 설정되어 있는 event loop에 자신의 __step() method를 호출해줄 것을 요청합니다. __step()은 자신의 _coro변수(coroutine객체)를 이용하여 해당 coroutine을 실행하는 메소드입니다. 이러한 과정을 coroutine이 task로서 실행되도록 event loop에 예약한다라고 표현할 수 있습니다.

task객체를 생성하는 asyncio.run(), asyncio.create_task()함수를 호출할 때 coroutine객체를 인자로 넘겨주고, 넘겨준 coroutine으로 task객체가 생성되면서 event loop에 해당 coroutine이 task로 실행되도록 예약됩니다.

 

task객체의 __step()메소드가 호출되면 coroutine이 실행되고, 해당 coroutine은 await키워드로 다른 coroutine을 호출할 수있고, 이러한 방식으로 여러 coroutine의 chain이 형성됩니다.

이러한 연쇄 과정으로 coroutine을 호출하다보면 sleep혹은 I/O관련 coroutine을 await하는 코드를 마주칠 수 있습니다. task는 이러한 상황을 감지하면 자신의 실행을 중단하고, event loop에게 제어권을 넘깁니다. event loop는 제어권을 얻으면 task(coroutine) queue에서 우선순위가 높은 것을 선택하여 실행하고, I/O바운드 작업이 완료된 task가 다시 실행 가능한 상태가 되면 이 task는 다시 event loop에 실행을 예약하여 task queue에 들어갑니다.

 

task객체가 처음 실행한 coroutine의 실행이 완료되면, 즉 해당 coroutine이 모든 yield 키워드를 소진한 상태에서 return 함으로써 StopIteration 예외가 발생하고, 그 coroutine객체로부터 반환 값을 얻어 자신(task)의 결과 값을 업데이트합니다.

이는 task의 실행이 완료 됨을 의미하며, 해당 task는 더 이상 event loop에 예약할 필요가 없습니다.

(asyncio.run()함수의 실행은 coroutine에 의해 실행되는 task의 실행이 완료될 때 까지 대기를 의미합니다)

 

 

Task 객체 실행 순서
1. asyncio.run()이나 asyncio.create_task()를 호출하게 되면 태스크 객체가 생성됩니다.
생성됨과 동시에 __step() 메서드를 호출할 것을 요청한다.

 

2. __step()은 자신의 코루틴 객체를 이용하여 해당 코루틴을 실행하는 메서드이다.
👉 코루틴이 태스크로써 실행되도록 이벤트 루프에 예약을 건다고 함

 

3. __step() 메서드를 통해 코루틴 실행이 개시되며, 처음 실행된 코루틴은 await 키워드를 이용하여 또 다른 코루틴을 부르거나 그 코루틴이 또다시 다른 코루틴을 부를 수도 있다. 👉 코루틴 체인


4. 코루틴을 연속적으로 호출하다보면 sleep이나 I/O 관련하여 await하는 코루틴을 마주칠 수 있다. 이 떄 태스크 객체는 자신의 실행을 중단하고 이벤트 루프에게 제어권을 넘긴다.
이벤트 루프는 자신에게 실행을 예약해둔 태스크들 중 우선순위가 높은 것을 적절히 선택하여 이를 실행하게 되고 아까 실행 중이던 태스크가 다시 실행할 수 있는 상태가 되면 이 태스크는 다시 이벤트 루프에게 실행을 예약해둔다.

5. 모든 yield 키워드를 소진한 상태에서 return시 StopIteration 예외가 발생하게되면 코루틴 실행이 완료된 것이다. 그러면 그 객체로부터 반환 값을 얻고 자기 자신(태스크 객체)의 결과 값을 업데이트 한다. 자기 자신의 결과값을 업데이트 한다는 것은 태스크의 실행이 완료되었다는 뜻이며 이제 더이상 이벤트 루프에 의해 실행이 예약될 수 없는 상태를 의미한다.
참고로 asyncio.run() 함수가 실행되는 것은 이로 인해 실행된 태스크의 실행이 완료될 때까지를 의미하는 것이다.


 

 

 

참고한 링크: https://it-eldorado.tistory.com/159?category=749661 

 

[Python] 비동기 프로그래밍 동작 원리 (asyncio)

JavaScript와 달리 Python은 비동기 프로그래밍에 어색하다. 애초에 JavaScript는 비동기 방식으로 동작하도록 설계된 언어인 반면, Python은 동기 방식으로 동작하도록 설계된 언어이기 때문이다. 그래

it-eldorado.tistory.com