아는 만큼 보인다

Multi-processing과 multi-threading, 그리고 global interpreter lock (feat. concurrency and parallelism) 본문

Python

Multi-processing과 multi-threading, 그리고 global interpreter lock (feat. concurrency and parallelism)

계토 2023. 12. 5. 16:36

Multi-processing과 multi-threading은 일종의 멀티태스킹을 달성하기 위한 두가지 다른 방법이라 할 수 있다. 한 번에 하나의 일만 해야 한다고 생각하면, 모든 작업들은 너무 느려질 수 밖에 없다.

 

이 둘은 어떻게 다른가? 보다 깊은 이해를 위해 작은 개념부터 시작해보려 한다.

 

1. Process vs Thread

Process

운영 체제로부터 시스템 자원을 할당받는 작업의 단위, 실행되고 있는 컴퓨터 프로그램의 instance (an instance of a program that is being executed). 하나의 cpu는 한번에 한개의 process를 실행시킨다.

 

할당받는 시스템 자원의 예시로, 독립된 메모리 영역(code, data, stack, heap), 운영되기 위해 필요한 주소공간 등이 있다. 

 

(참고: stack-프로그램의 active subroutine/function에 대한 정보를 저장하고 있는 stack 자료구조, heap-프로그램 실행동안 생성되는 중간 데이터를 hold하기 위한 메모리공간)

 

각 process는 별도의 주소 공간에서 실행되고, 한 프로세스는 다른 프로세스의 독립된 메모리 영역(변수나 자료 구조)에 접근할 수 없다(소통 불가!). 접근하고 싶으면 별도의 통신 방법을 이용해야 한다.

 

각 프로세스는 최소 1개의 thread를 가지고 있다. 또한 1개 이상의 thread를 가질 수 있다.

Thread

출처: https://www.javatpoint.com/process-vs-thread#:~:text=Thread-,A%20process%20is%20an%20instance%20of%20a%20program%20that%20is,are%20interdependent%20and%20share%20memory.

앞서 언급된 것처럼, 하나의 process의 구성 요소이다. process가 할당받은 자원을 이용하는 '실행(execution)'의 단위이다. 하나의 process 내에 있는 여러개의 thread는 code, data, heap의 영역은 서로 공유하고, stack, register, counter만 따로 할당받는다. 이렇게 자원을 공유하며 실행이 되어, 한 thread가 자원을 변경하면, 다른 thread도 그 변경 결과에 영향을 받는다. 

 

 

이 개념을 바탕으로 multi-processing과 multi-threading을 이해해보면, 말 그대로 multi-processing은 여러 개의 process를 실행시켜 여러 일을 한번에 처리하는 것이고, multi-threading은 여러 thread를 활용해 여러 일을 한번에 처리하는 것이다. 이 두 방식의 차이에 대해 조금만 더 파고들어가보자! 

 

2. Concurrency & Parallelism

일반적으로 multi-threading은 concurrency와 함께 많이 언급되고, multi-processing은 parallelism이라는 말과 함께 언급된다. 

 

Concurrency란, 여러 작업이 특정 순서 없이 overlapping time period (겹치는 시간 대에) 시작, 실행, 완료되는 것을 의미한다. 반대로 parallelism은 multi-core processor와 같은 여러 컴퓨팅 리소스가 있는 하드웨어에서 여러 작업이 말 그대로 '동시에/병렬적으로' 실행되는 것을 의미한다. 

 

Concurrency는 프로그램이나 시스템의 '의미론적 속성(semantic property)', '개념적 속성(conceptual property)', 즉 프로그램이나 시스템이 '어떻게 설계되었는지'와 관련된 것이(라고 한)다. 그리고 parallelism은 '실행 속성(implementation property)'이다. 즉, task가 물리적으로 동시에 실행되는 것을 의미한다.

 

좀 더 쉬운 표현으로 말하자면, concurrency는 프로그램이 여러 일을 한번에 '다룰 수 있는' 능력과 관련되어 있다(deal with many things at once). 반면 parallelism은 여러 일을 한번에 '하는' 것과 관련되어 있다(do many things at once).

 

예시로 알아보자.

 

우리가 '찌개 끓이기'와 '야채 썰기'를 동시에 해야한다고 하자. 찌개를 끓이기 위해서 필요한건 요리사가 찌개를 종종 저어주는 일이다. 만약 concurrency가 없다면, 찌개를 다 끓인 다음에 야채 썰기를 하거나, vice versa로 요리를 해야한다. 그러나 concurrency가 있다면, 찌개를 좀 저어주다가, 야채를 썰다가, 다시 찌개를 저어주다가, 야채를 써는 방식으로 두가지 일을 한번에 처리할 수 있다. 이것이 concurrency.

 

parallelism은, 찌개를 끓이는 사람과 야채를 써는 사람, 두 사람의 요리사가 있는 것이다. 

 

Concurrent 연산은 parallelized 될 수 있지만, concurrency가 parallelism 자체를 함축하는 것은 아니고, parallelism도 concurrency를 함축하고 있는 것은 아니다. concurrent 하면서도 parallel할 수 있고.....(이하 생략) 

 

이 둘의 관계성에 대한건... stackoverflow 링크 하나로 우선 대체하고자 한다. 이걸 다 요약 번역하는건 의미가 없을 것 같고 헷갈리는 게 있을 때마다 이걸 보면 될 것 같다. 정말 많은 사람들이 다양한 예시/그림으로 설명해주고 있다 (so kind ㅜㅜ). 

 

3. multi-threading vs multi-processing

원래 알아보고자 했던... multi-threading과 multi-processing으로 돌아와보자. 위를 다 이해하고 나면 보다 쉽게 이해될지도! 

 

multi-threading은, 한 프로세스 내에서 여러개의 thread를 사용하는 것으로, 프로세스가 concurrency를 달성하는 방법? 이라고 생각할 수 있다.  I/O intensive task에 좀 더 적절하다. 즉 읽을 파일이 10개이고, 읽은 파일의 데이터를 모두 모아 작업해야한다면, 메모리를 공유하는 multi-threading이 더 빠를 수 있다. 

 

multi-processing은, 여러 개의 cpu에 작업들을 각각 할당하여 여러 개의 프로세스로 병렬 처리하는 것이다. 계산량이 많은 일을 할 때, cpu intensive task를 할 때 더 적절하다. 그러나, 초기화 비용(시스템 자원 할당 등)이 많이 들어 단순한 작업을 할 땐 시간이 더 걸릴 수 있다. 

 

4. Python의 global interpreter lock (related to multi-threading)

Python에는 global interpreter lock (GIL)이라는 제약이 있다. 즉, 다수의 thread가 '병렬적으로' 실행될 수 없게 하는 기능이다. 한번에 하나의 thread만 실행될 수 있어, 한 시점에 하나의 thread에만 자원을 할당한 후에 lock을 걸어 다른 thread는 자원에 접근하지 못하게 막아버리는 것이다. 이에 각 thread는 순차적으로 일한다. 

 

이는 python의 garbage collection과 reference counting 과 관련되어 있다. Python은 garbage collection과 reference counting을 통해 메모리를 관리한다. 파이썬의 모든 객체는 변수로부터 참조된 수를 저장하고 있다(reference counting). 이 참조괸 수가 0이 되면, garbage collector가 그 객체를 메모리에서 삭제시킨다. 그래서 이를 정확하게 카운트하는 것이 중요한데, 여러 thread가 하나의 객체를 동시에 사용하면 이 counting을 관리하는 데에 문제가 생길수가 있고, 그럼 garbage collector가 부적절하게 동작할 수 있다. 그래서 모든 객체들의 reference counting 동기화 문제를 해결하고자 GIL이 사용되고 있다.

 

여기에 영향받지 않고 성능을 끌어올리려면 multi processing을 사용해야 하지만, I/O 작업이 큰 비중을 차지하거나 sleep으로 일정 시간 대기해야 하는 경우에는 multi-threading이 여전히 좋은 성능을 보일 수 있다.

 

5. 마지막 의문 - GIL이 왜 문제? 

자..... 여기서 이제 의문이 드는게..... concurrency 관점에서만, 그리고 single core 관점에서만 multi-threading을 이해하고 있었던 나. 그럼 왜 Python의 GIL이 문제인가? 원래 한번에 하나만 실행되는 거 아니냐!!!! - 여기서 진짜 많이 막혔다 ㅜㅜ 

 

그게 아니다 ㅎ 

 

그러니까, single core 환경에서는 thread가 concurrent하게'만' 실행되고 'parallel'하게 실행될 수는 없다. 그러나 multi-core 환경에서는, 자원을 공유하는 등의 multi-threading의 장점은 유지한채, 여러 개의 core가 달려 들어서(?) 각각의 thread을 실행시켜 parallel한 multi-threading을 가능하게 하는 것. 다시다시! multi-core system에서는, 한 프로세스의 여러 thread가 서로 다른 core에서 병렬적으로 실행될 수 있고, 이 경우에도 프로세스의 공유 메모리에 접근하고 공유 데이터 구조를 통해 서로 통신할 수 있다는 것.

 

이게 가능하니까, python에서는 GIL을 적용해서, 일종의 parallel multi-threading을 불가하게 한거고 concurrency만 달성되게 한 것이라고 볼 수 있을 것 같다. 그래서 멀티코어 환경이라도 python의 multi-threading 기능의 경우 single core 의 기능 정도 밖에 못내게 되는 현상이 일어난다. 그러나 여전히 위에서 언급되었듯 python에서도 multi-threading이 빠를 수 있는 경우가 있어서... 적절하게 사용하는 것이 중요하다. 특히 메모리 공유가 안되는 multi-processing을 잘못 활용하면, 메모리가 터질 수 있음~! (유경험자) 

 

실제로 다른 언어의 경우(예를 들어 GIL이 없고 full paralle execution of multi-thread를 지원하는 JAVA) concurrency control, data consistency와 관련된 보다 정교한 작업이 필요하다고 한다.

 

 

 

소회

쉽지 않은 여정이었다........언젠가 운영체제 공부를 추가로 더 해야할 듯 싶다.

 

 

출처

'Python' 카테고리의 다른 글

Generator란?  (0) 2023.11.30
파이썬 기초  (0) 2022.06.16