오늘도 개발

Asyncio와 비동기 작업 2) Awaitables(Coroutine, Task, Future) 본문

웹 프로그래밍/Python3

Asyncio와 비동기 작업 2) Awaitables(Coroutine, Task, Future)

Sueeeeeee 2022. 8. 24. 10:15

1. 네이티브 코루틴

asyncio에서 제공하는 async, await을 사용해서 만든 코루틴을 네이티브 코루틴이라고 한다. 

코루틴은 서브루틴(일반 함수)과 생김새가 비슷하지만 다음과 같은 차이가 있다.

서브루틴 

서브루틴은 def 키워드를 사용해서 만들 수 있다.

def 키워드로 만든 함수는 callable한(=호출 가능한) 오브젝트이다.

이 함수를 호출하면 함수 안의 코드를 실행해서 return 값을 반환한다.

 

아래 코드의 subroutine_example은 callable한 오브젝트이다.

넷째줄에서 subroutine_example은 즉시 실행되고, return 값을 sub에 저장한다.

def subroutine_example(x, y):
   return x + y

sub = subroutine_example(1, 2)
# sub은 3

코루틴

코루틴은 async def 키워드를 사용해서 만든다.

async def 키워드로 만든 함수는 서브루틴과 마찬가지로 callable한 오브젝트이다.

하지만 이 함수를 그냥 호출하면 함수 안의 코드가 실행되지도 않고 return 값을 반환하지도 않는다.

async def 키워드로 만든 함수를 호출하면 Coroutine 오브젝트를 반환한다.

 

아래 코드의 coroutine_example은 callble한 오브젝트이다.

하지만 넷째줄에서 coroutine_example은 실행되지 않고 return 값을 반환하지 않는다.

대신 coroutine_example()은 Couroutine 오브젝트를 반환해서 co에 저장한다.

def coroutine_example(x, y):
   return x + y

co = coroutine_example(1, 2)
# co는 <coroutine object coroutine_example at 0x10e80eb20>

코루틴을 실행해서 값을 얻으려면 await나 gather를 사용해서 호출해야 한다.

 

2. Await과 Awaitables(Coroutine, Future, Task)

await

await는 특정 오브젝트가 끝나기를 기다려야 할 때 사용한다.

await A의 뜻은,

- 현재 Task는 A가 끝난 이후에만 진행할 수 있으니 기다려라

- A가 끝날 때까지 다른 Task를 진행하고 있으라 

awaitables

await 다음에 올 수 있는 오브젝트는 awaitable(=기다릴 수 있는 것)이라고 부른다. 

await 다음에 올 수 있는 awaitable은 다음과 같다.

1) Coroutine 오브젝트 

2) Future 오브젝트

3) Task 오브젝트

 

3. await와 Coroutine 오브젝트

Coroutine 오브젝트

coroutine 함수. 또는 coroutine 함수가 반환한 Coroutine 오브젝트

 

await Coroutine 오브젝트

await를 Coroutine 오브젝트 앞에 붙이면 해당 코루틴을 현재 task에서 실행하고 값을 반환한다.

async def coroutine_example(x, y)
   return x + y

co = await coroutine_example(1, 2)
print(co)
# 3

 

4. await와 Future 오브젝트

Future 오브젝트

Future 오브젝트는 다른 곳에서 진행되고 있는 프로세스를 나타낸다.

asyncio.Future 클래스의 인스턴스이다.

 

Future 오브젝트 만들기

Future 오브젝트는 이렇게 만들 수 있다. 하지만 Future 오브젝트를 직접 만들 일은 잘 없다.

f = asyncio.get_running_loop().create_future()

 

await Future 오브젝트

await을 Future 오브젝트 앞에 붙이면 다음과 같은 일이 일어난다.

- Future가 끝나지 않았다면 => 끝날 때까지 현재 Task를 중단

- Future가 끝나서 값을 반환했다면 => await을 사용한 곳으로 값을 받음

- Future가 실행 중 exception을 일으켰다면 =>  await을 사용한 곳으로 exception을 받음

 

Future 오브젝트의 메서드

Future 오브젝트는 done, exception, result 메서드를 갖는다.

# f는 Future 오브젝트라고 가정

f.done()
# Future가 완료된 경우 True 반환

f.exception()
# Future가 완료되지 않은 경우 asyncio.InvalidStateError exception 반환
# Future가 완료되었고 오류가 난 경우 해당 오류 반환
# Future가 오류 없이 완료된 경우 None 반환

f.result()
# Future가 완료되지 않은 경우 asyncio.InvalidStateError exception 반환
# Future가 완료되었고 오류가 난 경우 해당 오류 반환
# Future가 오류 없이 완료된 경우 해당 값 반환

 

5. await와 Task 오브젝트

Task 오브젝트

코루틴을 실행하는 오브젝트. 

asyncio.Future 클래스의 인스턴스로, Future의 일종이다.

done, exception, result와 같은 Future 메서드를 모두 사용할 수 있다.

 

Task 오브젝트 만들기

Task 오브젝트는 create_task 메서드를 사용해서 만들 수 있다. 

create_task의 인자에는 Task가 실행할 코루틴을 넣는다.

create_task를 호출하면

- 인자로 넣은 코루틴을 새 Task로 만들고, event loop에 Task를 등록한다. (=코루틴 실행을 예약한다.) 

- Future 오브젝트를 반환한다. Task가 코루틴 실행을 완료하면 Future의 done이 True로 바뀐다.

 

아래와 같이 create_task를 하고 수동으로 event loop를 시작하면 await을 쓰지 않아도 task가 실행된다.

event loop에서 가장 먼저 실행할 task로 등록했기 때문이다. 

async def coroutine_example(x, y):
   return x + y

# 새 event loop 생성
loop = asyncio.get_event_loop()

# task 생성하고 event loop에 추가 
# t에 create_task의 반환값 future 저장
t = loop.create_task(coroutine_example(1, 2))
print(t)
# <Task pending name='Task-27513' coro=<coroutine_example() running at <ipython-input-267-5797f7eb5958>:1>>

# event loop 활성화 
# task t가 실행됨
# task t의 반환값 result에 저장
result = loop.run_until_complete(t)
print(result)
# 3
print(t)
# t에 저장된 futurer의 done이 True로 바뀌어 있음
# <Task finished name='Task-27513' coro=<coroutine_example() done, defined at <ipython-input-267-5797f7eb5958>:1> result=3>

 

await Task 오브젝트

await을 Task 오브젝트 앞에 붙이면 eventloop에 있는 해당 Task를 깨워 Task 오브젝트에 등록한 코루틴을 실행한다.

async def coroutine_example(x, y):
   return x + y

# 새 event loop 생성
loop = asyncio.get_event_loop()

# event loop에 task 생성
t = loop.create_task(coroutine_example(1, 2))
# t는 <Task pending name='Task-16478' coro=<coroutine_example() running at <ipython-input-157-5797f7eb5958>:1>>

# task t 실행 요청, event loop가 자동으로 활성화되고 task t 실행 
# task t의 반환값 result에 저장
result = await t
# result는 3
# t는 <Task finished name='Task-16478' coro=<coroutine_example() done, defined at <ipython-input-157-5797f7eb5958>:1> result=3>

create_task는 동기 함수이므로 동기 코드에서도, 비동기 코드에서도 사용할 수 있지만

비동기 코드 블럭에서 사용하는 것이 권장된다.

- 비동기 함수 내에서 create_task를 호출하는 경우 : 

   event loop가 이미 돌아가고 있다는 뜻 => 현재 실행중인 비동기 함수가 await하거나 끝나면 새로 만든 Task 실행 

- 동기 함수 내에서 create_task를 호출하는 경우 :

   event loop가 돌아가지 않고 있을 확률이 높음 => 수동으로 event loop를 활성화해야 함 => 파이썬 공식 문서에서 추천하지 않음 

   

gather()

create_task는 한 번에 한 task만 event loop에 넣는다.

여러 task를 한 번에 event loop에 넣고 싶을 때는 gather()를 사용하면 된다.

 

6. 비동기 프로그램 실행하기

asyncio.run(coro)

인자로 받은 코루틴 coro를 실행하는 메서드. 

항상 새로운 event loop를 생성한다. 

실행중인 event loop가 존재하면 오류가 난다. 

async def coroutine_example(x, y):
   return x + y
   
result = asyncio.run(coroutine_example(1, 2))
# result는 3

 

7. 수동으로 event loop 조작하기

동기식으로 작성된 코드 내에서 비동기 함수를 호출하는 경우,

event loop가 활성화되지 않았을 것이므로 수동으로 event loop를 활성화해야 함 

 

방법 1) 영원히 event loop를 돌리는 메서드. 잘 사용하지 않는다.

asyncio.get_event_loop().run_forever()

방법 2) 인자로 넣은 Future 오브젝트가 실행을 완료할 때 까지만 event loop를 돌리는 메서드.

# f라는 Future 오브젝트가 있다고 가정.
r = asyncio.get_event_loop().run_until_complete(f)

방법 2 적용 예시)

async def coroutine_example(x, y):
   return x + y

# 새 event loop 생성
loop = asyncio.get_event_loop()

# event loop에 task 생성
t = loop.create_task(coroutine_example(1, 2))

# t가 완료될 때까지 event loop 활성화
result = loop.run_until_complete(t)
# result는 3

 

7. 수동으로 현재 task 재우고 다른 task 깨우기

비동기 프로그램에서 await를 사용하면 자동으로 현재 task를 재우고 다른 task를 깨운다.(=control을 yield한다)

그래서 수동으로하는 yield control을 할 일은 잘 없고, 주로 디버깅을 할 때 정도만 사용한다.

asyncio.sleep()을 사용하면 yield control을 할 수 있다.

 

asyncio.sleep(3)은 3초동안 현재 task를 재우고 Future를 반환한다.

Future의 상태는 실행 직후 '진행중' 상태였다가 3초가 지나면 자동으로 '실행 완료'가 된다. 

 

task가 잠든 3초동안 파이썬은 event loop에 대기중인 다른 task를 실행한다. 

3초가 지나고 원래 task의 future는 '실행 완료' 상태를 갖지만,

그러거나 말거나 실행중인 task가 await하거나 끝나야만 원래 task로 돌아간다.

await asyncio.sleep(3)

asyncio.sleep()의 인자로 0을 넣는 경우도 있다.

이 경우 대기중인 Task가 있으면 대기중인 Task를 실행하고, 없으면 아무 일도 일어나지 않는다.

 

8. 실습

여러 코루틴을 event loop에 등록하고 비동기적으로 실행해보기.

async def counter(name):
    for i in range(3):
        print(f"{name} : {i}")
        # event loop에 대기중인 다른 task에게 control yield
        await asyncio.sleep(0)


async def main():
    tasks = []
    tasks.append(asyncio.create_task(counter("task1")))
    tasks.append(asyncio.create_task(counter("task2")))

    while True:
        print("14행")
        # 실행 완료한 task는 삭제
        tasks = [task for task in tasks if not task.done()]    
            
        # 모든 task가 실행 완료되어 삭제되었으면 프로그램 종료
        if len(tasks) == 0:
            return
        # 첫 번째로 대기중인 task 실행
        print("22행")
        await tasks[0]

asyncio.run(main())
# 14행
# 22행
# task1 : 0
# task2 : 0
# task1 : 1
# task2 : 1
# task1 : 2
# task2 : 2
# 14행

 

await 코루틴과 await 태스크의 차이 비교해보기

await 코루틴 : 코루틴을 현재 태스크에서 차례대로 실행 => 비동기적 실행 안 됨. 

# await 코루틴의 경우
async def co(x, y):
    print(x + y)
    await asyncio.sleep(2)
    return x + y

async def main():
    # 현재 task에서 코루틴 호출
    # 한 코루틴이 끝나야 다음 코루틴 호출
    # 총 6초 걸림
    c1 = await co(1, 2)
    c2 = await co(1, 3)
    c3 = await co(1, 4)

asyncio.run(main())

await 태스크 : 여러 코루틴을 각기 다른 새 태스크로 예약 등록 => 비동기적으로 실행됨.

# await 태스크인 경우
async def co(x, y):
    print(x + y)
    await asyncio.sleep(2)
    return x + y

async def main():
    # task로 코루틴 호출 예약
    # 한 코루틴 실행하다 await 만나면 다른 코루틴 
    # 코루틴 세 개 실행하는데 1초도 안 걸림
    t1 = asyncio.create_task(co(1, 2))
    t2 = asyncio.create_task(co(1, 3))
    t3 = asyncio.create_task(co(1, 4))

asyncio.run(main())

gather() : await 태스크와 같음. 코루틴을 태스크로 등록할 때 일일이 create_task를 쓰지 않아도 되게 해주는 문법.

from asyncio import gather

async def co(x, y):
    print(x + y)
    await asyncio.sleep(2)
    return x + y

async def main():
    # await을 안 붙였으므로 코루틴 실행 안 됨
    # 변수에는 코루틴 객체가 저장됨
    c1 = co(1, 2)
    c2 = co(1, 3)
    c3 = co(1, 4)
    # task로 각 코루틴 호출 예약
    result = await gather(c1, c2, c3)
    # result는 [3, 4, 5]로 저장됨

asyncio.run(main())

 

 

 

참고

python 공식문서 - asyncio

Python Asyncio Part 2 - Awaitables, Tasks, and Futures