Python/Python tip

Multiprocessing VS Threading VS AsyncIO in Python

bono. 2021. 2. 3. 07:32

Introduction

현대 컴퓨터 프로그래밍에서, 동시성(concurrency)은 문제를 보다 더 빨리 해결하기 위하여 필요합니다. 여기서 동시성이란 개념은 여러 작업들을 마치 '동시에 실행되는 것 같아 보이게' 실행하는 방법을 말합니다. 실제 여러 작업을 동시에 수행하는 병렬(parallel)과는 조금 다른 개념입니다. 굉장히 비슷해 보이는데, 그림 1을 참고하시면 이해하기 쉬울 것 입니다. 한대의 커피 머신에 두 줄로 선 사람들이 번갈아 가면서 커피를 받는 상황(concurrency)과 두 대의 커피 머신이 존재는 상황(parallel)입니다. 다시 본론으로 들어가 Python에서는 동시성을 수행하기 위하여 multi processing, threading, 그리고 asyncio 라는 library를 주로 사용합니다. 최근 Lei Mao는 python 의 동시성 처리가 기존 전통적인 언어인 c, c++과는 조금 다르게 동작 한다는 것을 알게 되었습니다.

 

Real python은 python 동시성 처리에 대해 코드 예제와 함께 tutorial을 제공하고 있습니다. 이 블로그 포스트에서는 multi processing, threading, 그리고 asyncio 에 대해 Real python에서 언급하지 않은 것들을 high-level에서 다루어 보려 합니다.

 

그림 1. 동시성과 병렬성에 대한 예시 https://joearms.github.io/published/2013-04-05-concurrent-and-parallel-programming.html

 

CPU-Bound VS I/O-Bound

현대 컴퓨터 (프로그래밍이 더 적절할 듯) 에서 해결하고자 하는 문제들은 CPU-bound 방법과 I/O bound 문제로 분류 할 수 있습니다. Cpu-bound, I/O bound는 동시성 library인 multi processing, threading, 그리고 asyncio 선택에 영향을 줍니다. 물론 우리가 설계한 문제에 따라 CPU-bound → I/O bound로, I/O bound → CPU-bound로 바뀔 수도 있습니다. CPU-bound와 I/O bound는 모든 언어, 프로그래밍에서 사용 되는 개념이므로, 이에 대해 설명하고 넘어가려 합니다.

CPU-Bound

CPU-Bound란 작업을 완료하는데 걸리는 시간이 중앙 processor에 의하여 결정되는 것을 말합니다. 따라서 CPU의 성능이 작업의 속도를 좌우하게 됩니다. 그림 2는 CPU-Bound를 위한 single processor, single thread 동기화를 보여 줍니다. 대부분의 단일 컴퓨터 프로그램은 CPU-Bound 입니다. 즉, CPU-Bound는 CPU에 의해서 작업이 처리되는 경우를 말합니다.

 

그림 2. CPU-Bound에 대한 도식화 https://realpython.com/python-concurrency/

 

I/O-Bound

I/O-Bound는 작업을 완료하는데 소요되는 시간이 주로 입/출력 완료 시간을 기다리는 것에 소요된 시간에 의하여 결정 되는 것을 말합니다. 이는 CPU-Bound와 반대되는 작업입니다. (? 머지) . CPU 성능이 좋아진다고 해서 I/O-Bound의 성능이 올라가지는 않습니다. 반대로 빠른 I/O를 할 수 있는 메모리, hardware, network를 가져 더 빠른 I/O-Bound를 가진다고 해서 CPU-Bound의 성능이 올라가지는 않습니다. (물론 이러면 I/O-Bound의 성능은 올라갑니다.) 대부분의 웹서비스는 I/O-Bound 입니다.

 

그림 3. I/O-Bound 도식화 https://realpython.com/python-concurrency/

 

Process VS Thread in Python

Process in Python

Python에서의 process를 설명하기에 앞서 Global Interpreter Lock(GIL) 이란 것을 짚고 넘어가려 합니다. 이는 여러개의 Thread가 있을 때 동기화하기 위하여 사용하는 기술입니다. GIL은 전역으로 Lock을 두고, 이 Lock을 점유 해야만 작업이 수행 될 수 있도록 제한합니다. 따라서 동시에 하나 이상의 thread가 실행 될 수 없습니다. 즉, 아무리 분산 처리를 해도 실제로는 하나의 thread (실제 물리적 cpu core)를 사용한다는 말입니다.

 

Python은 GIL을 사용합니다. 단일 프로세스 python 프로그램은 하나의 thread만을 사용합니다. 즉, single process - single thread 나 single process - multi thread를 사용하더라도 CPU의 성능을 100% 사용 할 수 없습니다. (물론 하나의 thread만 가지는 말도 안되는 성능의 cpu가 있다면 이야기가 다르겠지만...) C/C++ 와 같은 전통적인 언어에서는 GIL을 사용하지 않으므로, 100%를 넘는 CPU 성능을 이용 할 수 있습니다. (thread 여러개 쓸 수 있습니다)

따라서 python으로 CPU-Bound 프로그램을 만든다면, multi-process로 프로그램을 구현해야 합니다. (GIL 때문에!)

 

Thread in Python

단일 python 프로그램은 하나의 thread만 사용 가능합니다. single process - multi thread python 프로그램에서 몇개의 thread를 사용 하는 것과 상관없이, 해당 프로그램의 CPU 성능은 100%를 넘을 수 없습니다. (하나의 코어만 사용 가능하다는 뜻, 코어 하나가 100%)

 

그러므로 python 에서 CPU-Bound 작업을 하는 경우 single process - multi thread 가 성능을 향상시켜주지는 않습니다. 그렇다고 해서 python에서 multi thread가 쓸모 없다는 이야기는 아닙니다. I/O-Bound 작업을 하는 경우, multi-thread를 사용한다면 성능을 향상 시킬 수 있습니다.

 

 

Multiprocessing VS Threading VS AsyncIO in Python

Multiprocessing

python의 multi processing library를 사용하면 python multi-processing을 구현 할 수 있습니다. (Not multi thread) Multi-process python 프로그램은 각 thread에 여러개의 python interpreter를 만듦으로써, 모든 cpu 코어와 thread를 사용 할 수 있습니다. Python multi-processing에서 각 작업들은 독립적이기 때문에, 메모리를 공유하지 않습니다.

따라서 multi processing을 사용해 메모리를 공유하는 프로그램을 구현하기 위해서는, OS에서 제공하는 API를 사용해아 합니다. 그런데 이런 방법으로 구현하게 되면 큰 overhead가 발생하게 될 것 입니다. 그림 4. 를 보면 multi processing이 어떻게 동작을 하고 있는지 이해 할 수 있습니다. 다중 CPU를 이용하여 각각의 interpreter를 만들고, 프로그램을 실행합니다. 즉, multi processing은 CPU-Bound인 프로그램을 구현하는데 있어서는 성능을 발휘 할 것 입니다.

 

그림 4. Python multi-processing, 실행 도식화 https://realpython.com/python-concurrency/

 

Threading

Threading library를 사용한다면, I/O를 기다리며 idle 상태에 놓여진(프로세스가 긴 시간의 I/O를 만나게 되면 cpu가 idel 상태가 됩니다.) cpu를 더 효율적으로 이용 할 수 있습니다. Request 대기 시간을 중복하는 형태로 구현 되므로, 성능 향상이 가능합니다 (그림5. 참조).

하지만 Threading를 사용하면, 모든 thread가 메모리를 공유하므로, 작업 시 주의해야 하며 필요에 따라 lock을 사용해야 합니다. Lock / Unlock은 하나의 thread가 하나의 memory를 차지하게 만들어주지만, 약간의 overhead가 발생합니다. Threading의 thread는 cpu의 물리적인 core에 의하여 결정되는 thread와 다르고, 더 많을수도 있습니다 (일종의 논리 개념을 이용 한듯). 따라서 I/O-Bound 작업을 하는데 있어 Threading는 좋은 선택지가 될 수 있습니다.

 

그림 5. I/O Bound 에서의 Single process - multi thread https://realpython.com/python-concurrency/

 

 

AsyncIO

Threading을 사용한다면, multi-threading을 통하여 I/O Bound 성능 향상이 되지만 과연 multi-threading이 필요한가에 대하여 짚어 볼 필요가 있습니다. 만약 각 테스크들의 switch 시점을 알고 있다면, multi-threading을 이용 할 필요가 없습니다. 예를 들자면, Threading을 사용하는 python 프로그램의 각 thread는 결과 반환 시점과 request를 요청한 사이에 idle 상태가 됩니다. 따라서 I/O request가 요청 된 시간을 알 수 있다면 idle을 기다리지 않고 보다 효율적으로 다른 작업으로 전환 할 수 있으며, 하나의 thread가 이러한 작업을 관리해야 합니다(작업 management). Thread management에 대한 overhead가 없다면 더 빠른 I/O Bound 작업 속도를 얻을 수 있고, 이는 AsyncIO를 사용하면 됩니다.

 

Asyncio을 사용한다면 idle 상태의 cpu를 더 잘 사용 할 수 있습니다. Threading과는 달리 Asyncio는 single process-single thread을 이용합니다. Asyncio에는 작업의 진행 상황을 반복적으로 측정하는 'event loop' 가 존재하여 각 작업들을 효율적으로 관리해주므로, I/O 대기 시간이 효율적으로 관리됩니다. 이러한 과정을 cooperative multitasking 이라고도 합니다.

 

Asyncio의 단점은 추가적으로 각 테스크들의 작업을 추적 할 수 있도록 명시적으로 프로그래밍 해줘야한다는 것이 있습니다.

 

그림 6. AsyncIO...  https://realpython.com/python-concurrency/

 

Summary

 

Reference

[1] https://leimao.github.io/blog/Python-Concurrency-High-Level/

[2] https://realpython.com/python-concurrency/