CPython

Cpython 병렬성과 동시성 /(1) 멀티 프로세싱

25G 2023. 2. 12. 21:14

CPython은 병렬성과 동시성에 대해 다양한 접근방식을 제공한다. 주어진 상황에 따라 적절한 방식을 고르면 된다.
버마다의 장단점이 있는 각각의 동시성 구현을 주어진 상황에 따라 선택해서 사용할 수 있다.
Cpython은 기본적으로 네가지 모데을 제공한다.

  1. threading : 동시실행 O 병렬실행 X
  2. multiprocessing : 동시실행 O 병렬실행 X
  3. asyncio : 동시실행 O 병렬실행 X
  4. subinterpreters : 동시실행 O 병렬실행 X

프로세스의 구조

운영체제는 실행 중인 프로세스를 제어할 책임이 있다. 그것이 사용자 인터페이스 앱일 수 도 있고 네트워크 서비스나 운영체제 서비스 처럼 백그라운드로 동작할 수 도 있다.
운영체제는 프로세스를 제어하기 위해 새 프로세스를 시작하는 API를 제공한다. 프로세스가 생성되면 프로세스는 운영체제에 등록되어 운영체제가 실행 중인 프로세스를 알 수 있게 된다. 그렇게 프로세스는 저마다 고유한 아이디인 PID를 부여받고 운영체제에 따라서는 부가적인 프로퍼티가 추가되기도 한다.

POSIX프로세스는 운영체제에 등록된 최소한의 프로퍼티를 가진다.

  • 제어터미널
  • 현재 작업 디렉터리
  • 유효 그룹 아이디와 사용자 아이디
  • 파일 디스크립터와 파일 모드 생성 마스크
  • 프로세스 그룹 아이디와프로세스 아이디
  • 실제 그룹 아이디와 사용자 아이디
  • 루트 디렉터리

컴퓨터CPU는 프로세스를 실행할 때 다음과 같은추가 데이터가 필요하다.

  • 실행 중인 명령이나 명령을 실행하는데 필요한 다른 데이터를 보관하는 레지스터
  • 프로그램 시퀀스의 어떤 명령을 실행중인지 저장하는 프로그램 카운터

그리고 프로세스를 병렬로 실행하려면 주로 다음 두가지 방법을 사용할 수 있다.

  1. 다른 프로세스를 포크하기
  2. 스레드를 스폰하기

POSIX?

POSIX(포직스, /ˈpɒzɪks/)는 이식 가능 운영 체제 인터페이스(移植可能運營體制 interface, portable operating system interface)[1]의 약자로, 서로 다른 UNIX OS의 공통 API를 정리하여 이식성이 높은 유닉스 응용 프로그램을 개발하기 위한 목적으로 IEEE가 책정한 애플리케이션 인터페이스 규격이다.
https://ko.wikipedia.org/wiki/POSIX

멀티프로세스를 활용한 병렬 실행

POSIX 시스템은 포크 API를 제공한다.
어떤 프로세스든 이 API를 사용해 자식 프로세스를 포크할 수 있다. 프로세스 포크는 실행중인 프로세스가 호출 할 수 있는 저수준 운영체제 API이다
포크 호출이 일어나면 운영체제는 현재실행중인 프로세스의 모든 어트리뷰트를 복제해 새 프로세스를 생성한다. 이때 부모의 힙과 레지스터, 카운터위치도 새 프로세스로 복제된다. 자식프로세스는 포크 시점에 부모 프로세스에 존재하던 모든 변수를 읽을 수 있다.

멀티프로세스를 활용한 병렬 실행의 가장 큰 단점은 자식 프로세스가 부모 프로세스의 완벽한 복제본이란 것이다. CPython의 경우 프로세스를 포크하면 ㄷ개 이상의 CPython 인터프리터가 실행되고 각 인터프리터가 따로 모듈과 라이브러리를 불러들이면서 상당한 오버해드가 발생한다. 멀티 프로세스는 처리중인 작업의 크기가 프로세스를 퐄할때의 오버헤드보다 클때 사용하는 것이 좋다.
또 다른 단점은 포크된 프로세스의 힙이 부모 프로세스로부터 격리되어 있다는 것이다. 즉 자식 프로세스는 부모 프로세스의 메모리 공간에 쓰기를 수행할 수 없다.
자식 프로세스가 생성되면 자식 프로세스는 부모의 힙을 읽어 들일 수 있지만 부모프로세스에 정보를 다시 보낼때는 IPC를활용해야한다.

프로세스 스폰과 포크

multiprocessing 패키지는 세가지 병렬 프로세스 시작 방법을 제공한다.

  1. 인터프리터 포크
  2. 새 인터프리터 프로세스 스폰
  3. 포크 서버를 실행한 후에 원하는 만큼의 프로세스를 포크하기

윈도우와 macos에서는 스폰을 기본으로사용하고 리눅스에서는 포크를 사용한다.

import multiprocessing as mp
import os

def to_celsius(f):
    c = (f - 32) * (5/9)
    pid = os.getpid()

if __name__ = '__main__':
    mp.set_start_method('spawn')
    p = mp.Process(tartget=to_celsius, args=(110,))
    p.start()

multiprocessing API는 한 프로세스뿐 아니라 여러개의 프로세스를 시작하고 프로세스에 데이터를 제공할 수 있는 편리한 방법들을 제공한다. Pool클레스도 그중 하나입니다.

import multiprocessing as mp
import os

def to_celsius(f):
    c = (f - 32) * (5/9)
    pid = os.getpid()

if __name__ = '__main__':
    mp.set_start_method('spawn')
    with mp.Pool(4) as pool:
        pool.map(to_celsius, range(110, 150, 10))    

자식 프로세스 생성과정

pickle?

pickle 모듈은 파이썬 객체용 직렬화 패키지 이다.

기본적인 프로세스는 운영 체제가 자식 프로세스를 생성하면 생성된 프로세스는 부모 프로세스의 초기화 데이터를 기다린다.
부모 프로세스는 파이프 파일 스트림에 두개의 객체를 쓴다. 파이프 파일 스트림은 명령줄에서 프로세스 간에 데이터를 전송하는 특별한 I/O스트림이다. 부모 프로세스가 전송한 첫번째 객체는 준비 데이터 객체이다. 이 객체는 실행 디렉터리, 시작방법, 명령줄 인자, sys.path 같은 부모 프로세스 정보의 일부를 담고 있는 딕셔너리다. 두번째 객체는 BaseProcess의 자식 클래스 인스턴스다. 호출 방식과 운영 체제에 따라 BaseProcess의 자식 클래스 중 하나를 인스턴스 화 하고 직렬화 한다.
준비데이터와 프로세스 객체 모두 pickle로 직렬화 되어 부모 프로세스의 파이프 스트림으로 전송된다.

부모 프로세스는 위와같은 과정들을 통해 모듈과 실행할 함수를 직렬화하고 자식 프로세스도 이 과정을 통해 전송된 인스턴스를 직렬화 하고 함수를 인자와 함께 호출하고 반환한다.
자식 프로세스가 시작된 이후에는 Queue와 Pipe를 확장한 객체를 이용해 데이터를 교환해야한다.
프로세스를 풀에서 생성한 경우, 생성된 첫번째 프로세스는 준비가 완료된후 대기상태ㅗ 들어간다. 부모프로세스는 이과정을 반복하면서 다음 워크에 데이터를 전송한다.
초기화 후에는 어떤 데이터든 큐와 파이프로만 교환할 수 있다

큐와 파이프를 사용해 데이터 교환하기

프로세스간 통신에는 작업 특성에 따라 큐와 파이프 두가지 방법을 사용할 수 있다. 이때 세마포어라는 변수를 사용하여 자원을 적절하지 못한 접근으로 부터 보호한다.

세마포어

다양한 멀티프로세싱 메커니즘들이 자원이잠겼거나 대기중이거나 잠기지 않았다는 신호를 보내는 방법으로 세마포어를 사용한다. 운영 체제는 파일과소캣등 자원을 잠그기 위한 단순한 가변타입으로 이진 세마포어를 사용한다.
이는 한 프로세스가 파일이나 네트워크 소켓을 쓰고 있을 때 다른 프로세스가 같은 파일에 쓰기시작하면 데이터는 바로 손상되기 때문에 이를 방지하기 위함

프로세스는 잠금이 해제되기를 기다리고 있다는 신호를 보낼 수 있고, 잠금이 해제되면 잠금이 해제되어자원을 사용할 수 있다는 메시지를 받게 된다.

큐는 여러 프로세스 간에 작은 데이터를 주고 받기 좋은 방법이다.
multiprocessing 패키지에서 먼저 부모 프로세스가 inputs 큐에 입력값을 삽입하면 첫번째 워커가 큐에서 객체를 꺼낸다. .get()을 사용해 큐에서 객체를 꺼낼 때는 큐 객체는 세마포어 잠금을 사용한다.

ex)

  1. 첫번째 워커가 작업 중이면 두번째 워커가 큐에서 다음값을 꺼낸다.
  2. 첫번쩨 워커는 계산을 완료하고 결과를 outputs 큐에 삽입한다.
    두 큐는 각각 출력과 입력을 위해 사용된다. 최종적으로는 모든 입력값이 처리되고 outputs 큐가 가득 채워진다. 이후 부모 프로세스가 결과값들을 출력한다.

파이프

multiprocessing 패키지는 Pipe 타입을 제공한다. 파이프를 인스턴스화 하면 부모 쪽 연결과 자식 쪽 연결, 두개의 연결이 반환된다. 두 연결 모두 데이터를 보낼수도 받을수도 있다. 그렇기 때문에 프로세스가 겹치게 되어 데이터가 꼬일 수도 있는데 이때 세마포어를 통해 파이프를 통한 작업간에 데이터가 꼬이지 않도록 한다.

요약

멀티프로세싱은 확장 가능한 CPU 집약적인 작업을 병렬 작업으로 쪼개서 멀티코어 또는 멀티 CPU컴퓨터의 장점을 활용할 수도 있다.
CPU 집약적인 작업이 아닌 I/O집약적인 작업의 경우에는 멀티프로세싱이 적합하지 않다. 예를 들어 워커 프로세스 4개를 스폰하고 같은 파일을 일고 쓸 경우, 한 프로세스가 작업하고 있는 동안 나머지 프로세스 3개는 잠금이해제 되기를 기다려야한다.
또한 멀티 프로세싱은 새로운 파이썬 인터프리터를 시작하는데 필요한 시간과 처리 오버헤드로 인해 짧은 작업에는 그다지 적합하지 않다. I/O 작업과 짧은 작업의 시나리오에서는 멀티 스레딩이 유리하다.