Python의 병렬 처리, 개념(만) 이해해보기

SeunghoChoe·2021년 2월 27일
1

개인적으로 Python의 공식문서를 공부하며 정리한 글입니다.

Python문서의 병렬 처리장 구성

문서에서 병렬 처리 장은 크게 8장으로 구성되어 있다.

  1. threading - 스레드 기반 병렬 처리
  2. multiprocessing - 프로세스 기반 병렬처리
  3. multiprocessing.shared_memory - 프로세스 간 직접 액세스를 위한 공유 메모리를 제공
  4. concurrent 패키지
  5. concurrent.futures - 병렬 작업 실행하기
  6. subprocess 서브 프로세스 관리
  7. sched - 이벤트 스케줄러
  8. queue - 동기화된 큐 클래스

위에서 언급된 단어들에 대한 개념들을 다루어보려고 합니다. 여러 문서를 참고할 것이기에 다소 난잡 할 수 있는 부분 염두해두시길 바랍니다!

1. threading

멀티쓰레딩 방식의 프로그래밍을 지원합니다. 다만 Python은 GIL(Global Interpreter Lock)로 인하여 멀티 코어 시스템의 이점을 누리지 못합니다. 그럼에도 I/O병목이 많은 테스크에 경우에는 유효한 방식입니다. (왜냐하면 I/O자원에 접근하기 위해 Blocking이 일어난 경우에 다른 쓰레드가 CPU자원을 점유할 수 있기 때문입니다.)

1.1 Thread의 정의

A thread is a basic unit of CPU utilization; it comprises a thread ID, a program counter, a register set, and a stack. It shares with other thread beloging to the same process its code section, data section, and other operating-system resources, such as open files and signals.

쓰레드는 CPU를 활용하는 기본적인 단위로써,
thread ID, program counter, register set, stack으로 구성되어 있다.
쓰레드는 같은 프로세스에 속한 다른 쓰레드들과 코드 영역과 데이터 영역, 그리고 운영체제의 다른 자원들(File, Signal)을 공유합니다.

2. multiprocessing

쓰레드 대신 자식 프로세스 생성을 통해서 병렬 처리를 지원하는 모듈입니다. 멀티 프로세스를 이용한 프로그래밍은 쓰레드와 달리 Python의 GIL을 회피하여 멀티 코어 시스템을 극대화 할 수 있는 방식입니다.

2.1 Process 정의

A process is the unit of work in a modern time-sharing system.
A process is a program in execution

프로세스는 시간을 공유하는 현대 시스템(컴퓨터)에서 일의 단위이다.
프로세스는 실행된 프로그램이다.

책에서는 process, task, job의 용어를 정리하는 내용이 나오는데, 사실 학술적인 관점에서 각 용어의 의미를 정확하게 파악하진 못했고, 다만 process를 설명하는 맥락에선 각 용어들을 혼용해서 사용한다고 한다.

프로세스는 코드 영역(Text Section), 데이터 영역(Data Section), 힙(Heap), 스택(Stack)으로 구성되며, 코드 영역은 프로그램 카운터와 프로세서의 레지스터 내용을 포함한다. 자세한 메커니즘은 생략..

2.2 Multicore Programming

컴퓨터의 초기 모델은 싱글 코어의 시스템이였다. 그러나 성능 개선 요구에 따라 컴퓨터는 멀티 코어 시스템으로 발전하였고 여러 개의 코어를 하나의 칩에 위치시켰다. 멀티코어 프로그래밍은 멀티코어(혹은 멀티프로세서)시스템의 CPU사용을 극대화하기 위해서 하나의 CPU에 여러 개의 프로세스(또는 쓰레드)를 바꿔가며 할당하는 방식이다.

3. multiprocessing.shared_memory

통상적으로 프로세스들은 각각 상호 배타적인 메모리 공간을 갖지만, 공유 메모리 공간을 활용할 수도 있습니다. 해당 모듈은 그러한 공유 메모리 기능을 제공하며, 공유 메모리를 사용할 경우 디스크(파일)나 소켓 등의 다른 통신과 비교하여 성능상의 이점을 얻을 수 있습니다.

3.1 Shared-Memory Systems

일반적으로 운영체제는 하나의 프로세스가 접근하는 특정 메모리 영역에 대해서 다른 프로세스들의 접근을 제한한다. 그러나 Shared-Memory는 여러 개의 프로세스가 동시에 접근이 가능한 메모리 영역이다. 데이터의 형식과 위치는 운영 체제가 아닌 프로세스들에 의해서 결정되어지고, 이 프로세스들은 데이터가 동시에 Write되지 않도록 작동할 책임을 갖는다.

4. concurrent

4.1 병렬 실행(Concurrency)과 동시 실행(Parallelism)의 차이

동시 실행(Parallelism)은 둘 이상의 테스크가 동시에 처리되는 경우이다. 반면에 병렬 실행(Concurrency)은 둘 이상의 테스크에 대해서 모든 테스크들이 같이 진행되도록 처리하는 경우이다. 따라서 테스크들(프로세스)을 동시에 실행하지 않으면서 병렬로 실행하는 것은 가능하다.

멀티 코어 아키텍처가 등장하면서 동시 실행이 가능해졌고, 이전에 싱글 코어 아키텍처 환경에서는 프로세스를 전환하면서 병렬 처리만이 가능했다. 다만 프로세스의 전환이 빠르게 이루어지기 때문에 동시 실행이 되는 듯한 착각을 불러 일으켰다.

5. concurrent.futures

concurrent.futures모듈은 쓰레드와 프로세스에 대해서 풀(Pool) 기능을 제공합니다.

5.1 Thread Pool

병렬 처리 프로그램을 구현하기 위해서는 대표적으로 동기화(Synchronization), 교착 상태(Daedlock)와 같은 어려운 주제를 다루어야 하며, 이 뿐만 아니라 테스크를 식별하고, 쓰레드 간의 균형을 맞추는 등 여러 과제 또한 처리해야 한다. Pool은 이러한 어려움들을 처리하기 위한 여러 접근법 중 하나이다. (책에서는 Thread Pools를 비롯한 OpemMP, Grand Central Dispatch를 나열하고 있다.)

Pool의 기본 개념은 미리 생성된 일정 수의 Thread가 테스크를 기다리는 공간입니다. Thread들은 Process가 시작되는 시점에 생성되어 Pool에 담겨집니다. (웹 서버를 예로 들어)서버가 요청(Request)을 받으면 사용가능한 Thread를 꺼내와서 해당 Thread에 요청을 할당하고, 작업이 끝나면 다시 Pool에 되돌려 놓습니다. 이용가능한 Thread가 없을 경우에는 다른 Thread의 작업이 끝나 Pool에 돌아올 때까지 기다립니다.

이러한 Pool을 이용하면 다음 3가지의 이점이 있습니다.

  1. 요청이 올때마다 Thread를 생성하는 것보다 시간 비용을 줄일 수 있습니다.
  2. Thread의 총량을 관리할 수 있습니다.
  3. 테스크의 생성과 실행을 분리할 수 있습니다.

6. subprocess

subprocess모듈은 서브 프로세스(자식 프로세스)를 생성하고 입력, 출력, 에러 파이프 라인을 연결하고, 상태 값을 반환 받는 작업들을 지원하는 모듈입니다.

6.1 프로세스의 생성

하나의 프로세스는 다른 여러 개의 프로세스 들을 생성할 수 있으며, 생성한 프로세스를 부모 프로세스, 생성된 프로세스를 자식 프로세스라고 부릅니다. 이러한 모든 프로세스들의 관계를 도식화하면 트리(Tree)형태가 됩니다.

부모 프로세스는 CPU, 메모리, 파일 등과 같은 자원들을 자식 프로세스와 공유할 수 있으며, 초기화 데이터(입력값)를 자식 프로세스에게 전달 할 수도 있습니다.

부모, 자식 두 프로세스를 실행시키는 방식은 2가지가 있습니다.

  1. 부모와 자식이 병렬적으로 실행되는 것
  2. 부모가 자식의 종료를 기다렸다가 실행되는 것

자식이 생성되는 시점에는 부모와 동일한 메모리 공간과 데이터를 갖습니다. 그러나 exec()가 실행되는 순간에 새로운 프로그램이 메모리 공간에 적재 됩니다. 이
러한 맥락에서 자식의 메모리 공간상의 데이터는 2가지 경우가 있습니다.

  1. 부모 프로세스의 복사본
  2. 새로운 프로그램

부모 프로세스는 wait()를 이용하여 자식 프로세스의 종료를 기다릴 수 있습니다. 부모 프로세스가 wait()로부터 작업을 재개할 때는 자식 프로세스가 작업을 종료하고 exit()를 호출하는 시점입니다.

6.2 프로세스의 종료

프로세스는 exit()호출을 통해해서 운영체제에게 자신의 삭제를 요청하고 종료됩니다. 프로세스가 종료되면 가상 메모리, 파일, 입출력 버퍼 등의 모든 자원이 운영체제에게 반납되며, 부모 프로세스는 wait()를 통해서 상태 값(정수)을 반환 받을 수 있습니다.

자식 프로세스는 오직 부모 프로세스에 의해서 종료되어 질 수 있으며, 이는 pid값을 기반으로 이루어집니다. 부모는 다음과 같은 이유들로 자식 프로세스를 종료시킵니다.

  1. 자식이 할당된 자원을 초과하여 사용할 때
  2. 자식에게 할당된 태스크가 더이상 필요하지 않을 때
  3. 운영체제 차원에서 부모 프로세스가 종료된 이후에 자식 프로세스가 지속되는 것을 허용하지 않을 때

6.3 좀비 프로세스와 고아 프로세스

프로세스의 종료 상태가 저장되는 프로세스 테이블의 항목은 부모 프로세스가 wait()를 호출할 때까지 유지됩니다. 부모 프로세스가 wait()를 호출하지 않을 경우 종료된 자식 프로세스는 좀비 프로세스가 됩니다.

부모 프로세스가 wait()를 호출하지 않고 종료된다면 자식 프로세스는 고아 프로세스가 됩니다. 이 경우 운영체제에 따라 대응이 달라지는데 Linux와 UNIX의 경우 고아 프로세스를 루트 프로세스의 자식 프로세스로 지정합니다.

7. sched

해당 모듈에서 이벤트 스케줄러는 일정 시간 이후 이벤트를 발생시키는 모듈로써, 프로세스 스케줄러와는 전혀 무관합니다.

8. queue - 동기화된 큐 클래스

해당 모듈은 쓰레드 간에 안전한 정보 전달을 지원하는 큐를 제공합니다. 해당 모듈은 록(Lock)을 사용하여 경쟁 쓰레드를 일시적으로 블록(Block)시킵니다.

8.1 Synchoronized Queue(Blocking Queue)

To be done

9. 용어 정리

9.1 문서에서의 용어 오용


'Concurrent Execution'장 아래에 threading과 multiprocessing장 제목으로 'parallelism'이 사용되었다. 사실상 어플리케이션 레벨에서 CPU에 프로세스 또는 쓰레드를 할당을 제어하지 않기 때문에 Python에서 동시 실행(Parallelism)이란 단어를 사용한 것은 부적합하다고 생각합니다.

참고 문헌

profile
I'm a software engineer

0개의 댓글