파이썬의 비동기 라이브러리 비교

5 분 소요

번역글입니다*

파이썬에서 사용할 수 있는 비동기 라이브러리인 multiprocessing, threading, 그리고 asyncio에 대한 내용에 대하여 찾다가 잘 정리된 글이 있어, 해당 포스팅에 대한 번역(적당히 의역)한 내용입니다.


소개

현대 컴퓨터 프로그래밍에서는 빠르게 문제 해결을 하기 위해서 종종 동시성이 필요합니다. 파이썬 프로그래밍에서는 우리는 일반적으로 동시성을 달성하기 위한 세 가지 라이브러리 옵션인 multiprocessing, threading, 그리고 asyncio을 사용할 수 있습니다. 최근에 저는 스크립팅 언어로서 동시성 동작이 C/C++과 같은 기존의 컴파일된 언어에 비해 미묘한 차이가 있다는 것을 알고 있었습니다.

Real Python은 이미 Python 동시성에 대한 코드 예제와 함께 좋은 튜토리얼를 제공했습니다. 이 블로그 게시물에서는 Real Python 튜토리얼에서 언급하지 않은 몇 가지 추가적인 주의 사항과 함께 multiprocessing, threading, asyncio 기능에 대하여 설명하고자 합니다. 나는 또한 그들의 튜토리얼에서 좋은 다이어그램을 구하여 사용하였으며, 독자들은 그 삽화들에 대한 점수를 그들에게 주어야 합니다.

CPU-Bound vs I/O-Bound

현대의 컴퓨터가 해결하려는 문제는 일반적으로 CPU-Bound 또는 I/O-Bound로 분류될 수 있습니다. 이러한 분류는 동시성을 처리하기 위한 라이브러리의 선택을 하는데 있어 영향을 미칩니다. 물론 일부 시나리오에서는 문제를 해결하기 위한 알고리즘 설계로 인해 문제가 CPU-Bound 에서 I/O-Bound로, 또는 그 반대로 변경될 수 있습니다. 이러한 개념은 모든 프로그래밍 언어에 보편적입니다.

CPU-Bound

CPU 바운드는 작업을 완료하는 시간이 주로 CPU의 속도에 따라 결정되는 조건을 의미합니다. CPU의 클럭속도가 빠를 수록 프로그램의 성능은 향상됩니다.

CPU 바운드의 단일 프로세스, 단일 스레드 동작

단일 컴퓨터 프로그램의 대부분은 CPU에 종속됩니다. 예를 들면, 숫자 리스트가 주어지는 경우 모든 숫자의 합을 계산하는 것을 들 수 있습니다.

I/O-Bound

I/O 바운드는 계산을 완료하는 데 걸리는 시간이 주로 입력/출력 작업이 완료될 때까지 대기한 기간에 의해 결정되는 조건을 의미합니다. 이것은 CPU 바인딩된 작업의 반대입니다. CPU 클럭속도를 높여도 성능은 향상되지 않습니다. 반대로, 더 빠른 I/O를 위한 메모리, 하드디스크, 네트워크성능이 향상되면 프로그램의 성능이 향상됩니다.

I/O 바운드의 단일 프로세스, 단일 스레드 동작

대부분의 웹 서비스 관련 프로그램은 I/O 바인딩을 사용합니다. 예를 들어 레스토랑 이름 목록이 주어지면 Yelp에서 평점을 찾고, 결과가 나올때까지 대기하게 됩니다.

파이썬의 Process vs Thread

파이썬의 Process

글로벌 인터프리터 잠금(GIL)은 한 번에 하나의 네이티브 스레드만 실행할 수 있도록 스레드 실행을 동기화하기 위해 컴퓨터 언어 인터프리터에서 사용되는 메커니즘입니다. GIL을 사용하는 인터프리터는 멀티 코어 프로세서에서 실행되더라도 항상 한 번에 하나의 네이티브 스레드만을 실행할 수 있습니다. 여기서 네이티브 스레드는 프로그래밍 언어의 스레드 개념이 아닌 물리적 CPU 코어의 스레드 수입니다.

Python 인터프리터는 GIL을 사용하기 때문에 단일 프로세스 Python 프로그램은 실행 중에 하나의 네이티브 스레드만 사용할 수 있습니다. 즉, 단일 프로세스 Python 프로그램은 단일 프로세스-단일 스레드든 단일 프로세스-다중 스레드든 상관없이 CPU를 100% 이상 활용할 수 없습니다(기본 스레드의 전체 활용도는 100%로 정의됨). C/C++와 같은 기존 컴파일된 프로그래밍 언어에는 GIL은 물론 인터프리터가 없습니다. 따라서 단일 프로세스 다중 스레드 C/C++ 프로그램의 경우 많은 CPU 코어와 많은 네이티브 스레드를 활용할 수 있으며 CPU 활용률이 100% 이상일 수 있습니다.

따라서 Python의 CPU 바인딩 작업의 경우 성능을 극대화하기 위해 다중 프로세스 Python 프로그램을 작성해야 합니다.

파이썬의 Thread

왜냐하면 단일 프로세스 Python은 하나의 CPU 네이티브 스레드만 사용할 수 있기 때문입니다. 단일 프로세스 Python 프로그램에서 얼마나 많은 스레드가 사용되었든 간에, 단일 프로세스 멀티 스레드 Python 프로그램은 최대 100%의 CPU 활용률만 달성할 수 있었습니다.

따라서 Python의 CPU 바인딩 작업의 경우 단일 프로세스 다중 스레드 Python 프로그램은 성능을 향상시키지 않습니다. 그러나 이것이 Python에서 멀티 스레드가 무용지물이라는 것을 의미하지는 않습니다. Python에서 I/O 바인딩된 작업의 경우 프로그램 성능을 높이기 위해 다중 스레드를 사용할 수 있습니다.

Multiprocessing VS Threading VS AsyncIO in Python

Multiprocessing

Python 멀티 프로세싱을 사용하여 여러 프로세스를 사용하여 Python을 실행할 수 있습니다. 원칙적으로 다중 프로세스 Python 프로그램은 많은 네이티브 스레드에 여러 Python 인터프리터를 만들어 사용 가능한 모든 CPU 코어와 네이티브 스레드를 충분히 활용할 수 있습니다. 모든 프로세스가 서로 독립적이고 메모리를 공유하지 않기 때문입니다. Python에서 멀티 프로세싱을 사용하여 협업 작업을 수행하려면 운영 체제가 제공하는 API를 사용해야 합니다. 따라서 약간의 오버헤드가 있을 것입니다.

CPU 바운드를 위한 멀티프로세스

따라서 Python의 CPU 바인딩 작업의 경우 멀티 프로세싱은 성능을 최대화하는 데 사용할 수 있는 완벽한 라이브러리가 될 것입니다.

Threading

Python Threading을 사용하면 I/O를 기다릴 때 유휴 상태로 있는 CPU를 더 잘 사용할 수 있습니다. 요청 대기 시간을 겹침으로써 성능을 개선할 수 있습니다. 이때 모든 스레드가 동일한 메모리를 공유하기 때문에, Python에서 스레딩을 사용하여 협업 작업을 수행하려면, 우리는 주의해야 하며 필요할 때 잠금을 사용해야 합니다. 잠금 및 잠금 해제는 한 번에 하나의 스레드만 메모리에 쓸 수 있도록 합니다. 하지만 이 경우 오버헤드도 발생합니다. 여기서 설명한 스레드는 CPU 코어의 네이티브 스레드와는 다릅니다. CPU 코어의 네이티브 스레드 수는 현재 일반적으로 2개이지만 단일 프로세스 Python 프로그램의 스레드 수는 2개보다 훨씬 클 수 있습니다.

I/O 바운드를 위한 단일프로세스 멀티스레드

따라서 Python의 I/O 바인딩 작업의 경우 Threading을 사용하면 성능을 극대화할 수 있습니다. 다만, 모든 스레드가 풀에 있으며 운영 체제의 실행자가 스레드를 관리하여 실행할 사용자와 실행 시기를 결정한다는 점에 유의해야 합니다. 이것은 운영 체제가 실제로 각 스레드에 대해 알고 있고 언제든지 인터럽트하여 다른 스레드 실행을 시작할 수 있기 때문에 Threading의 단점일 수 있습니다. 운영 체제가 스레드를 선점하여 전환할 수 있기 때문에 이를 선점형 멀티태스킹이라고 합니다.

AsyncIO

Python에서 I/O 바인딩된 작업의 성능을 극대화하기 위해 Threading을 사용하는 경우 멀티 스레드를 사용해야 하는지 여부를 알 수 있습니다. 그러나 스레드의 작업 전환이 언제되는가를 아는가에 대한 질문에 답은 아니요 입니다. 예를 들어 Threading을 사용하는 Python 프로그램의 각 스레드에 대해 요청이 전송되고 결과가 반환되는 사이 실제로 유휴 상태를 유지합니다. 만약 어떻게든 스레드가 I/O 요청이 전송된 시간을 알 수 있다면 유휴 상태를 유지하지 않고 다른 작업을 수행하도록 전환할 수 있으며 하나의 스레드로 이러한 모든 작업을 관리하기에 충분합니다. 스레드 관리 오버헤드가 없으면 I/O 바인딩된 작업의 실행 속도가 빨라집니다. 확실히 이러한 것을 Threading은 할 수 없지만, 이를 위하여 우리에게는 asyncio가 있습니다.

Python asyncio를 사용하면, I/O 대기 시 유휴 상태로 있는 CPU를 보다 효율적으로 사용할 수 있습니다. Threading과 다른 점은 비동기를 사용하는 방식이 단일 프로세스, 단일 스레드라는 점입니다. asyncio에서는 작업 진행률을 측정하는 이벤트 루프가 있습니다. 이벤트 루프가 진행 상황을 측정한 경우 실행을 위해 다른 작업을 예약하기 떄문에 I/O 대기에 소요되는 시간이 최소화 됩니다. 이를 협력적 멀티태스킹이라고 합니다. 작업은 언제 교체될 준비가 되었는지 발표하여 협력해야합니다.

I/O 바운드를 위한 단일프로세스 단일스레드

asyncio의 단점은 우리가 말하지 않으면 이벤트 루프는 진행상황을 알지 못합니다. 이것은 우리가 asyncio를 사용할 때 약간의 추가 노력이 필요하다는 것을 의미합니다.

요약

세 가지 비동기 방식을 아래와 같이 정리할 수 있습니다.

방식 특징 사용기준 비유
Multiprocessing 다중 프로세스, 높은 CPU 사용률 CPU-Bound 우리에게는 10개의 부엌, 10명의 요리사, 요리할 10개의 요리가 있습니다.
Threading 단일 프로세스, 다중 스레드, 선점형 멀티태스킹, OS가 스레드의 전환을 결정 빠른 I/O-Bound 우리에게는 1개의 부엌, 10명의 요리사, 요리할 10개의 요리가 있습니다. 10명의 요리사가 모이면 주방은 혼잡합니다.
AsyncIO 단일 프로세스, 단일 스레드, 협력적 멀티태스킹, 작업이 협력적으로 전환을 결정 느린 I/O-Bound 우리에게는 1개의 부엌, 1명의 요리사, 요리할 10개의 요리가 있습니다.

주의 사항

HTOP vs TOP

htop는 때때로 python 프로그램에 대하여 여러 PID를 표시하기 때문에 다중 스레드 Python 프로그램을 Multiprocessing 프로그램으로 잘못 해석할 수 있습니다. top에는 이 문제가 없습니다. StackOverflow에도 이러한 내용에 대한 관찰이 있습니다.

References

댓글남기기