Victoree's Blog

[7] 동시성 프로그래밍 - future와 asyncio 본문

Python/Fluent Python

[7] 동시성 프로그래밍 - future와 asyncio

victoree 2023. 6. 23. 17:13
728x90

7.1 Future

Future은 비동기 작업의 실행 객체이다. future 객체는 concurrent.futures 라이브러리나 asyncio에서 future 객체를 이용할 수 있다. 이 두 Future 클래스 객체는 완료되었을 수도, 아닐수도 있는 지연된 계산을 표현하기 위해 사용된다. Future는 앞으로 일어날 일을 나타내고, 이 실행을 스케줄링하는 프레임워크만이 어떤 일이 일어날지 확실히 알 수 있기 때문에 동시성 프레임워크에서만 배타적으로 생성해야한다. 또한 Client에서 Future의 상태를 변경하면 안된다. 실행 여부를 체크하려면 done() 메소드를 호출함으로 알 수 있고, 일반적으로 클라이언트는 Future가 완료되었는지 물어보는게 아니라 callback으로 통지해주는 것을 요청한다.

Asyncio나 concurrent Future 객체는 GIL에 의해 제한되기 때문에 단일 스레드로 실행된다. Cpython은 thread safe(어떤 공유 자원에 여러 쓰레드가 동시에 접근해도, 프로그램 실행에 문제가 없는 상태)하지 않아서 다중 CPU 코어를 사용할 수 없다. 하지만 IO 작업과 관련한 일을 수행할때는 GIL이 해제되기 때문에, 다른 스레드가 다시 작업할 수 있어 IO 작업을 할 때에는 Cpython 인터프리터로 작업을 해도 좋다. CPU 코어를 많이 사용해야하는 sorting이나 압축, 인코딩 같은 작업을 할 때에는 PyPy를 사용하던지 멀티 프로세스 기반으로 수행하는 것이 더 효과적이다.  

ThreadPoolExecutor와 ProcessPoolExecutor 클래스를 통해 서로 다른 스레드나 프로세스에서 콜러블 객체들을 실행할 수 있도록 해주는 인터페이스를 구현한다. ProcessPoolExecutor를 사용하는 경우, 병렬 컴퓨팅이 가능해지고 GIL을 우회하므로 CPU 코어를 많이 사용하는 계산 위주의 작업을 수행할 때 사용하면 좋다. 

7.2 asyncio

asyncio는 파이썬 3.4 표준 라이브러리에 추가되었고, 3.3과도 호환이 된다. yield from 표현식을 아주 많이 사용하므로 이전 버전의 파이썬과는 호환되지 않는다. asyncio는 이벤트 loop에 의해 운용되는 코루틴 기반 동시성 라이브러리이다

asyncio의 코루틴은 yield from 으로 호출하는 호출자에 의해 구동되거나, asyncio.async() || run_until_complete() 등의 함수에 전달해서 구동해야 한다. 또한 @asyncio.coroutine이라는 데코레이터를 코루틴에 적용해야한다. @asyncio.coroutine은 일반 함수와 다르게 보이도록 만들며, 코루틴이 yield from 되지 않고 가비지 컬렉트되는 경우 경고 메세지를 출력해줘 디버깅에 도움이 된다. @asyncio.coroutine은 제너레이터를 자동으로 기동해주지 않는다. 

asyncio의 Task는 threading.thread와 거의 동일하다. Task는 gevent와 같은 협업적 멀티태스킹을 구현하는 라이브러리에서의 그린 스레드와 같다. 

그린 스레드란, 런타임 라이브러리나 가상머신에서 스케줄링되는 thread를 말한다.

Task는 코루틴을 구동하고, asyncio.async() 나 loop.create_task()에 전달하여 가져온다. Task 객체를 가져오면 이 객체는 이미 asyncio.async() 등에 의해 이미 실행이 스케줄링 되어있다. 스레드는 외부에서 API를 통해 중단시킬 수 없다. 스레드를 아무때나 중단시키면 시스템 상태의 무결성이 훼손되기 때문이다. 스케줄러는 언제든 스레드를 중단시킬 수 있다. 프로그램의 critical section을 보호하기 위해 락을 잠그고, 여러 단계의 작업을 수행하는 도중 인터럽트가 되지 않게 해야한다. 코루틴의 경우, 모든 것이 interrupt로부터 보호되는데, 이는 yield를 명시적으로 실행해야 그 다음 부분이 실행되기 때문이다. 

aysncio를 사용할 때, 코루틴 체인을 만들고 가장 바깥 대표 제너레이터는 asyncio 자체에 의해 구동되며, 가장 안쪽 하위 제네레이터는 asyncio 라이브러리가 제공하는 코루틴에 위임한다. asyncio 이벤트 루프가 코루틴 체인을 구동하고, 그 체인은 비동기 라이브러리 함수에서 끝이나는 것이다 .

asyncio 기반으로 완전한 비동기를 구현하려고하면 모든 코드베이스가 비동기여야 한다. 모든 작은 코드 조각까지 하나하나 전부. 이것은 동기(synchronous) 함수가 너무 많은 시간이 걸려 이벤트 루프를 블로킹할 수도 있기 때문이다.

asyncio가 threading의 문제점들을 어떻게 극복했는지 살펴보자

  • CPU 컨텍스트 스위칭 : asyncio 는 비동기이며 이벤트 루프를 사용한다. 이는 I/O를 대기하는 동안 애플리케이션이 컨텍스트 스위치를 관리할 수 있도록 한다. CPU 스위칭이 없다.
  • 경쟁 조건 (Race Conditions) : asyncio 는 한 번에 오직 하나의 코루틴만 실행하며 정의된 지점에서만 스위칭이 일어나기 때문에, 코드는 경쟁 조건으로부터 안전하다.
  • 데드락/라이브 잠금 (Dead-Locks/Live-Locks) : 경쟁 조건에 대해 걱정할 필요가 없기 때문에, 잠금을 사용할 필요가 없다. 이는 데드락으로부터 매우 안전하게 만들어준다. 만약 두 개의 코루틴이 서로를 깨워야(wake) 할 필요가 있을 경우엔 여전히 데드락이 발생할 가능성이 있지만, 이런 일을 해야할 경우는 매우 드물 것이다.
  • 기아 상태 (Resource Starvation) : 모든 코루틴이 하나의 스레드에서 실행되고, 추가적인 소켓이나 메모리를 필요로하지 않기때문에, 되려 리소스가 부족하기가 힘들 것이다. 그러나 Asyncio 는 기본적인 스레드 풀인 “executor pool"을 하나 가지고 있다. 만약 매우 많은 일들을 하나의 “executor pool"에서 실행한다면, 여전히 리소스 부족에 대한 문제가 발생할 수 있다. 하지만, 매우 많은 실행 프로그램을 사용하는것은 안티 패턴이며, 아마 이런 일을 자주 하지는 않을 것이다.
728x90
Comments