1. Home
  2. 프로그래밍/Python
  3. [번역] 파이썬에서 멀티 프로세싱(Multiprocessing) VS 멀티 쓰레딩(Threading) VS 비동기(AsyncIO) 비교

[번역] 파이썬에서 멀티 프로세싱(Multiprocessing) VS 멀티 쓰레딩(Threading) VS 비동기(AsyncIO) 비교

참고 : 해당 글은 https://leimao.github.io/blog/Python-Concurrency-High-Level/ 을 번역한 내용입니다. 대부분 의역하였으며, 제가 이해를 돕고자 추가하거나 수정한 내용이 있을 수 있습니다.
또한 원문과 다르게 여기서 CPU 100%을 활용한다는 것은 GIL이 걸리지 않고 모든 쓰레드가 번갈아가면서 실행되며 모든 컴퓨터 자원을 사용하는 것을 의미합니다. 100% 활용하지 못한다는 것은 GIL이 걸려서 쓰레드가 1개만 실행되는 상황을 뜻합니다.

 

읽기전에 알아두면 좋은 내용

- 컴퓨터의 프로세스와 쓰레드 개념

[최대한 추가 설명을 덧붙였으나 아무래도 영어글이기도 하고 설명이 약간 불친절한 감이 있어서 가벼운 마음으로 읽으시는게 좋을 거 같습니다.]

 

개요

현대 컴퓨터 프로그래밍에서, 문제를 빠르게 해결하기 위해 종종 동시성(concurrency)이 필요합니다. 파이썬에서는 동시성을 구현하기 위해 멀티 프로세싱(Multiprocessing), 멀티 쓰레딩(Threading) 그리고 비동기(AsyncIO) 라는 3가지 선택지가 있습니다. 저는 최근에 스크립트 언어로써 파이썬의 동시성 동작이 C/C++ 과 같은 기존의 컴파일 언어와 미묘한 차이가 있음을 알게 되었습니다.

 

Real Python 사이트에서는 이미 파이썬의 동시성에 대해 코드 예제와 함께 좋은 튜토리얼을 제공하고 있습니다. 해당 블로그 글에서는 파이썬을 고수준 (High-Level) 관점에서, Real Python 튜토리얼에서 언급하지 않은 몇 가지 추가 주의 사항과 함께 [**multiprocessing**], [**threading**], [**asyncio**] 에 대해 논의해보려고 합니다.

 

또한 그들의 튜토리얼에서 몇 가지 좋은 다이어그램(사진)을 빌려왔고 독자들은 이 사진에 대한 후원금을 그들에게 주어야 합니다.

 

CPU Bound vs I/O Bound

현대 컴퓨터가 해결하려는 문제는 보통 CPU-bound 또는 I/O-bound로 분류될 수 있습니다. 그리고 문제가 CPU bound 인지, I/O bound 인지에 대한 여부는 우리가 동시성 라이브러리인 [**multiprocessing**], [**threading**], [**asyncio**] 중 어떤 것을 선택해야 할 지에 대해 영향을 끼칩니다. 물론 어떤 상황에서는 문제를 해결하기 위한 알고리즘 설계가 문제를 CPU-bound 에서 I/O bound로 바꿀 수도 있고 그 반대로도 바꿀 수 있습니다. CPU Bound 및 I/O Bound의 개념은 모든 프로그래밍 언어에서 보편적인 개념입니다.

 

CPU Bound

CPU Bound를 위한 싱글 프로세스, 싱글 스레드 동기화

CPU Bound란 작업을 완료하는데 걸리는 시간이 주로 CPU(중앙 처리 장치, 프로세서) 의 속도에 의해 결정되는 조건을 의미합니다. CPU의 클럭 속도가 빠를 수록 계산이 빨리 이루어지고 프로그램의 성능이 향상됩니다.

 

대부분의 단일 컴퓨터 프로그램은 CPU Bound 입니다. 예를 들어서 숫자들의 목록이 주어지면 모든 숫자들의 합을 계산하는 프로그램이 있습니다. (CPU가 주로 계산하는 프로그램)

 

I/O-Bound

I/O Bound는 작업을 완료하는데 걸리는 시간이 주로 입출력 작업이 완료되기를 기다리는 시간에 의해 결정되는 조건을 의미합니다. 이는 CPU-Bound 작업과 반대입니다. CPU 클럭 속도를 아무리 높혀도 프로그램의 성능이 크게 향상되지 않습니다. 반대로 더 빠른 메모리 I/O, 하드 드라이브 I/O, 네트워크 I/O 등 더 빠른 입출력이 있으면 프로그램의 성능이 향상됩니다.

 

대부분의 웹 서비스 관련 프로그램은 I/O Bound 입니다. 예를 들어서 웹 사이트에서 레스토랑 이름 목록을 "입력" 하면 평점을 "출력" 하는 네트워크 작업이 있습니다. [네트워크 I/O]

 

파이썬에서 Process VS Thread

파이썬에서 프로세스

GIL(Global Interpreter Lock)은 인터프리터에서 한 번에 하나의 네이티브 쓰레드만 실행할 수 있도록 쓰레드의 실행을 동기화 하는 데 사용하는 매커니즘 입니다. GIL을 사용하는 인터프리터는 멀티 코어 프로세서에서 실행되는 경우에도 한 번에 하나의 네이티브 쓰레드만 실행할 수 있습니다. 여기서 네이티브 쓰렐드는 프로그래밍 언어의 쓰레드 개념 대신 물리적 CPU 코어의 쓰레드 수를 나타냅니다.

 

파이썬 인터프리터는 GIL을 사용하기 때문에 단일 프로세스 파이썬 프로그램은 실행 중에 하나의 네이티브 쓰레드만 사용할 수 있습니다. 즉 단일 프로세스 파이썬 프로그램은 단일 프로세스 단일 쓰레드 또는 단일 프로세스 멀티 쓰레드와 관계 없이 CPU를 100% 활용할 수 없습니다. C/C++ 와 같은 기존 컴파일 언어는 GIL은 물론 인터프리터도 없습니다. 따라서 단일 프로세스 멀티 쓰레드 C/C++ 프로그램의 경우에는 많은 CPU 코어와 많은 네이티브 쓰레드를 사용할 수 있으며 CPU를 100% 활용할 수 있습니다.

 

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

 

파이썬에서 쓰레드

(위 내용에서 계속됨)

단일 프로세스 파이썬 프로그램은 CPU 네이티브 쓰레드를 하나만 사용할 수 있기 때문입니다. 단일 프로세스 파이썬 프로그램에서 얼마나 많은 쓰레드를 사용하던 간에 단일 프로세스 멀티 쓰레드 파이썬 프로그램은 CPU를 100% 활용할 수 없습니다.

 

따라서 파이썬의 CPU-Bound 작업의 경우 단일 프로세스 다중 쓰레드로 프로그램을 작성해도 성능을 향상시킬 수 없습니다. 그러나 이것이 파이썬에서 멀티 쓰레드가 쓸모 없다는 것을 의미 하진 않습니다. 파이썬의 I/O-Bound 작업의 경우 CPU-Bound 작업과 다르게 멀티 쓰레드를 활용하여 프로그램 성능을 향상시킬 수 있습니다.

 

파이썬에서 Multiprocessing VS Threading VS AsyncIO

CPU-Bound를 위한 멀티 프로세스

멀티 프로세싱(Multiprocessing)

파이썬의 Multiprocessing 을 활용하여 여러 프로세스를 사용해 파이썬을 실행할 수 있습니다. 원친적으로 다중 프로세스 파이썬 프로그램은 여러 네이티브 쓰레드에 여러 파이썬 인터프리터를 생성하여 사용 가능한 모든 CPU 코어와 네이티브 쓰레드를 완전히 활용 가능합니다.

 

왜냐하면 모든 과정은 독립적이고, 메모리를 공유하지 않기 때문입니다. 멀티 프로세싱을 사용하여 파이썬에서 협업 작업을 수행하려면 OS에서 제공하는 API를 사용해야 합니다. 따라서 오버헤드(부하) 가 약간 클 것 입니다.

 

따라서 파이썬의 CPU-Bound 작업의 경우 멀티 프로세싱은 성능을 극대화 하는데 사용하기에 완벽한 라이브러리 입니다.

 

쓰레딩 (Threading)

I/O 바운드를 위한 싱글 프로세스 멀티 쓰레드

파이썬의 쓰레딩(멀티 쓰레드) 을 활용하면 I/O 대기시 유후 상태의 CPU를 보다 효과적으로 사용할 수 있습니다. 요청 대기 시간을 겹쳐서 성능을 향상시킬 수 있습니다. 또한 모든 쓰레드가 동일한 메모리를 공유하기 때문에 [각주*현재 가정하는 상황은 파이썬 프로세스 1개에서 여러개의 쓰레드를 생성한 멀티 쓰레딩 상황입니다 기본적으로 한 프로세스 안에서 실행되는 여러 쓰레드는 전역 변수와 일부 메모리 공간을 공유할 수 있습니다.*]  쓰레딩을 이용하여 파이썬에서 협업 작업을 수행하려면 주의해야 하며 필요할때 Lock[각주*세마포어나 뮤텍스를 활용해 상호배제 하는 것을 의미하는듯*]을 사용해야 합니다. Lock과 Unlock 을 사용하면 한 번에 하나의 쓰레드만 메모리에 쓸 수 있지만 (임계 영역을 한 번에 하나의 쓰레드만 접근할 수 있음을 의미) 이 경우 부하도 발생합니다. 여기서 설명한 쓰레드는 CPU 코어의 기본 쓰레드와 다릅니다. 현재 CPU 코어의 네이티브 쓰레드 수는 보통 2개이지만 단일 프로세스 파이썬 프로그램의 쓰레드 수는 2개보다 훨씬 많을 수 있습니다.

 

따라서 파이썬의 I/O Bound 작업의 경우 멀티 쓰레딩을 활용하여 성능을 최대화 하는데 사용하는 적합한 라이브러리 후보가 될 수 있습니다. 또한 모든 쓰레드가 풀에 있으며 어떤 쓰레드가 언제 실행될 지 결정하는 OS의 실행자가 있습니다. [각주*이 실행자를 보통 OS에서 스케줄러라고 함.*] 이는 OS가 실제로 각 쓰레드에 대해 알고 있으며 다른 쓰레드 실행을 시작하기 위해 언제든지 중단할 수 있기 때문에 쓰레딩의 단점이 될 수 있습니다. 운영체제가 쓰레드를 선점하여 전환할 수 있기 때문에 이것을 선점형 멀티 태스킹이라고 합니다. 

 

AsyncIO

I/O Bound를 위한 단일 프로세스 단일 쓰레드 비동기 작업

파이썬에서 I/O Bound 작업의 성능을 최대화 하기 위해서 멀티 쓰레드를 사용하는 것을 고려할 때, 우리는 멀티 쓰레드를 활용할 필요가 있는지 궁금해집니다. 작업을 전환할 시기를 알고 있는 경우 대답은 No 입니다. 예를 들어 멀티 쓰레딩을 사용하는 파이썬 프로그램의 각 쓰레드에 대해 요청이 전송되고 결과가 반환되는 사이에 CPU는 유후 상태로 유지 됩니다. 쓰레드가 I/O 요청이 전송된 시간을 알 수 있다면 유후 상태를 유지 하지 않고 다른 작업으로 전환할 수 있으며, 이러한 모든 작업을 관리하기 위해 쓰레드 하나로 충분해야 합니다.

 

쓰레드 관리 오버헤드가 없으면 I/O Bound 작업의 실행 속도가 빨라집니다. 분명 멀티  쓰레딩은 할 수 없었지만, 우리에게는 asyncio가 있습니다.

 

파이썬의 asyncio 를 활용하면 I/O 대기 중인 유후 CPU를 더 효과적으로 사용할 수 있습니다. 쓰레딩과 다른 점은 비동기식은 단일 프로세스 및 단일 쓰레드라는 점입니다. 비동기식에서는 작업 진행률을 정기적으로 측정하는 이벤트 루프가 있습니다. 이벤트 루프가 진행률을 측정한 경우 다른 작업을 실행하도록 예약하므로 대기 I/O 에 소요되는 시간을 최소화 할 수 있습니다. 이를 협력적 멀티 태스킹이라고 합니다. 작업을 전환할 준비가 되었을때 발표함으로써 협력해야 합니다.

 

asyncio 의 단점은 이벤트 루프의 진행 상황을 우리가 말하지 않으면 알 수 없다는 점입니다. 이것은 우리가 asyncio를 사용하여 프로그램을 작성 시 추가적인 노력을 필요로 합니다.

 

요약

동시성 종류 특징 사용 기준 비유
Multiprocessing 다중 프로세스, 높은 CPU 사용률 CPU Bound 작업 10개의 주방, 10명의 요리사, 10가지 요리가 있습니다.
Threading 단일 프로세스, 멀티 쓰레드, 선점형 멀티 태스킹, 운영체제에 의해 OS 마음대로 쓰레드가 번갈아가면서 실행됨 빠른 I/O Bound 작업 (말 그대로 수행 시간이 빠른 것들) 우리에게는 하나의 주방, 10명의 요리사, 10가지 요리가 있습니다. 10명의 요리사가 모여서 일을 하게 되면 주방은 붐빕니다.
AsyncIO 단일 프로세스, 단일 쓰레드, 협력적 멀티 태스킹, 작업이 협력적으로 스위칭을 결정 느린 I/O Bound 작업 (말 그대로 수행 시간이 느린 것들) 주방 하나, 요리사 한 명 요리할 요리 열 가지가 있습니다.

 

리눅스의 HTOP vs TOP

htop 은 멀티 쓰레드 파이썬 프로그램을 가끔 멀티 프로세스 프로그램으로 잘못 해석합니다. 파이썬 프로그램에 대한 다중 PID를 보여줍니다. top에는 이 문제가 없습니다.

스택 오버플로우에선 이런 관점도 있습니다.


조금 더 쉬운 요약

- 파이썬에서 CPU 계산이 주가 되는 CPU Bound 작업의 성능을 올리고 싶으면 멀티 쓰레딩은 별 효용성이 없고, 프로세스를 여러개 생성하는 멀티 프로세스를 사용해야 함.

- 멀티 프로세스를 도와주는 모듈이 바로 Multiprocessing 임. 다만 방식 자체는 파이썬 프로그램에서 쓰레드를 여러개 만들어서 그 쓰레드 안에서 파이썬 인터프리터를 각각 실행시키는 방식이라 무거움

 

- 파이썬에서 I/O 작업이 주가 되는 IO Bound 작업의 성능을 올리고 싶으면 멀티 쓰레딩 또는 비동기(싱글 쓰레드, 이벤트 루프)를 활용하면 됨. * 물론 멀티 프로세스를 활용해도 되긴 하는데 프로세스 오버헤드가 너무 커서 비추합니다

 

- 멀티 쓰레딩을 도와주는 모듈은 Threading 이고, 비동기 작업을 도와주는 모듈은 AsyncIO 임.

 

참고 : 제가 HTTPS 요청을 한꺼번에 100개쯤 보내봤는데 Threading 이랑 Asyncio 둘다 써본 결과 아무래도 Asyncio 는 쓰레드 하나에서 이벤트 루프를 돌리는 형식이라 Threading(멀티 쓰레딩) 처럼 컨텍스트 스위칭 비용이 없어서 조금은 더 빨랐습니다.

 

웹 요청을 순식간에 아주 많이 보내야할 상황이 온다면 Asyncio 도 좋은 선택입니다. 다만 PyQT 같이 파이썬 GUI 프레임워크에서 Asyncio 를 결합시키는건 매우 어렵고, 서드 파티 라이브러리 (qasync) 를 사용했는데도 굉장한 UI 렉이 발생했습니다. 개인적으로 싱글 쓰레드 비동기 개념을 이전부터 사용해온 자바스크립트를 사용해서 프로그램을 제작하는게 비동기 코드 작성해선 가장 괜찮은 방법이였습니다. UI는 그냥 HTML 웹으로 짜면 끝이니깐요.

 


아시다 싶이 파이썬은 GIL이란게 걸려 있어서 멀티 쓰레드를 만든다고 해도 여러 쓰레드가 번갈아 가면서 파이썬 코드를 실행시키는게 아니라 오직 한 개의 쓰레드만 코드를 실행하게 됩니다.

 

그래서 분명 난 멀티 쓰레드 코드를 짰는데도 전체적인 흐름이 싱글 쓰레드 처럼 동작하며, 이 쓰레드간 전환(컨텍스트 스위칭)의 비용 때문에 어떨땐 오히려 싱글 쓰레드 보다 못한 성능이 나오게 됩니다 ㅋㅋㅋ;;;

 

여러 프로그래밍 언어로 코딩을 해보신 분들은 알겠지만 각 언어가 완벽한게 아니라 불편한점도 있고 꼭 나사빠진 부분이 하나씩 있습니다. 파이썬은 제가 생각하기에 굉장히 현대적이고 깔끔한 언어입니다만 골때리는 부분이 바로 이 GIL이라고 생각합니다. 

 

파이썬의 멀티 쓰레드는 거의 싱글 쓰레드나 다름 없는 수준으로 CPU Bound 작업에서는 전혀 이득을 볼 수 없지만 그럼에도 I/O Bound 작업에는 이득을 볼 수 있습니다. 왜냐하면 I/O 작업은 CPU 계산이 주가 되는게 아니라 대부분 내가 원하는 데이터를 입력하고 상대방이 데이터를 줄때까지 기다리는 작업이 대부분이기 때문입니다. GIL 때문에 쓰레드가 번갈아가면서 거의 동시에 실행되진 않아도 일단은 쓰레드가 여러개 돌아가고 있는건 맞아서 각 쓰레드에 I/O 작업을 동기적으로 할당하면 성능을 향상시킬 수 있습니다.

그리고 요즘 현대 프로그래밍 언어에서 핫 한 개념 바로 비동기 개념을 활용해도 좋습니다. 이 비동기는 아마 자바스크립트에서 Promise나 Call Back 함수를 통한 비동기 코드를 여러번 작성해보신 분들이면 미리 알고 계신 개념일 것인데 쓰레드 1개에서 이벤트 루프란 것을 돌려서 코드의 결과를 기다리지 않고 무차별적으로 실행시켜서 결과만 나중에 한꺼번에 추합해서 가져오는 형식입니다.

 

다만 현대 프로그래밍 언어에서 비동기가 모두 쓰레드 1개와 이벤트 루프로 동작하는건 지는 잘 모르겠습니다. 지금은 싱글 쓰레드, 멀티 쓰레드 개념에 대해 확실히 이해를 했습니다만 이 비동기와 동기 작업에 관해선 쓰레드와 멀티 쓰레드에 있어서 상관 관계를 잡기가 어렵네요.

 

지금까지 이해한 바로는 싱글 쓰레드, 멀티 쓰레드 안에서 코드가 실행되는 방식이 동기 / 비동기로 갈린다 정도만 이해하고 있습니다. 자바스크립트의 경우 비동기로 코드를 작성하면 싱글 쓰레드, 비동기 방식으로 작동하는 것이구요.

멀티 쓰레드 비동기.. 이것도 있을 거 같은데 아직은 이런 형태의 코드를 못본 거 같네요 ^^;;

SNS 공유하기
네이버밴드
카카오톡
페이스북
X(트위터)

최근글
인기글
이모티콘창 닫기
울음
안녕
감사
당황
피폐