Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 371 – 표준 라이브러리에 multiprocessing 패키지 추가

Author:
Jesse Noller <jnoller at gmail.com>, Richard Oudkerk <r.m.oudkerk at googlemail.com>
Status:
Final
Type:
Standards Track
Created:
06-May-2008
Python-Version:
2.6, 3.0
Post-History:
03-Jun-2008

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 pyProcessing [1] 패키지를 “multiprocessing”으로 이름을 바꾸어 Python 표준 라이브러리에 포함할 것을 제안합니다.

processing 패키지는 표준 라이브러리 threading 모듈의 기능을 모방하여 스레드 프로그래밍에 대한 프로세스 기반 접근 방식을 제공하며, 이를 통해 최종 사용자는 전역 인터프리터 잠금을 사실상 우회하는 여러 작업을 디스패치할 수 있습니다.

이 패키지는 객체와 작업의 원격 공유 및 관리를 위한 서버 및 클라이언트 기능(processing.Manager)도 제공합니다. 따라서 애플리케이션은 로컬 머신의 여러 코어를 활용할 수 있을 뿐만 아니라 네트워크로 연결된 머신 클러스터에 객체와 작업을 분산할 수도 있습니다.

이 패키지의 분산 기능도 유용하지만, 이 PEP가 주로 초점을 맞추는 것은 패키지의 핵심인 스레딩과 유사한 API와 기능입니다.

근거

현재 CPython 인터프리터는 전역 인터프리터 잠금(GIL)을 구현하고 있습니다. Python 3000이나 현재 계획된 다른 버전에서 이루어지는 작업 [2]을 제외하면, GIL은 가까운 미래에도 CPython 인터프리터 안에 지금 모습 그대로 남아 있을 것입니다. GIL 덕분에 인터프리터와 확장 기반의 C 코드를 깔끔하고 유지 관리하기 쉽게 작성할 수 있지만, 멀티 코어 머신을 활용하는 Python 프로그래머에게는 GIL이 자주 문제가 됩니다.

GIL은 어느 시점에서든 인터프리터 안에서 둘 이상의 스레드가 실행되지 못하게 막습니다. 이 때문에 Python은 사실상 멀티 프로세서 시스템을 활용하지 못합니다.

pyprocessing 패키지는 GIL을 우회하는 방법을 제공합니다. 그래서 사용자에게 프로그래밍 패러다임을 완전히 바꾸라고(즉, 스레드 프로그래밍을 버리고 Twisted, Actors 등 다른 “동시성” 접근 방식을 택하라고) 요구하지 않고도 CPython 애플리케이션이 멀티 코어 아키텍처를 활용할 수 있습니다.

Processing 패키지는 CPython에 “익숙한 API”를 제공합니다. 이 API는 PEP 8 규약을 따르도록 바뀌었을 뿐 threading API를 그대로 반영하며, 익숙한 의미 체계와 손쉬운 확장성을 갖추고 있습니다.

CPython 인터프리터가 앞으로 “진정한” 스레딩을 지원하게 되면 이 패키지의 중요성은 줄어들 수도 있습니다. 그러나 일부 애플리케이션에서는 경량 스레드를 사용하는 것보다 OS 프로세스를 포크하는 편이 더 나을 때가 있습니다. 프로세스 생성이 빠르고 최적화된 플랫폼에서는 특히 그렇습니다.

예를 들어, 간단한 스레드 애플리케이션은 다음과 같습니다:

from threading import Thread as worker

def afunc(number):
    print number * 3

t = worker(target=afunc, args=(4,))
t.start()
t.join()

pyprocessing 패키지는 API를 매우 충실하게 반영했기 때문에, import를 다음과 같이 간단히 바꾸기만 하면:

from processing import process as worker

이제 코드는 processing.process 클래스를 통해 실행됩니다. 물론 PEP 8 규약에 맞추어 API 이름이 바뀌면 사용자 애플리케이션 안에서도 이름을 추가로 바꾸어야 하겠지만, 그 정도는 사소합니다.

이러한 호환성 덕분에 (대부분의 경우) 코드를 조금만 바꾸면 사용자 애플리케이션은 해당 머신의 모든 코어와 프로세서를 병렬 실행에 활용할 수 있습니다. I/O 중심 프로그램에서는 pyprocessing 패키지가 일반적인 스레딩 방식보다 오히려 더 빠른 경우도 많습니다. 물론 이는 pyprocessing 패키지가 최적화된 C 코드로 작성된 반면 threading 모듈은 그렇지 않다는 점을 고려한 것입니다.

“분산” 문제

이 패키지를 포함하는 것에 관한 Python-Dev 논의 [3]에서는 이 PEP가 “분산” 문제를 해결하려 한다는 식으로 의도가 잘못 이해되었습니다. 그래서 이 패키지의 기능을 MPI 기반 통신 [4], CORBA, 기타 분산 객체 접근 방식 [5] 같은 다른 솔루션과 비교하는 경우가 잦았습니다.

“분산” 문제는 방대하고 다양합니다. 이 분야에서 일하는 프로그래머들은 저마다 선호하는 모듈이나 방법에 대해 매우 확고한 의견을 가지고 있거나, 기존의 어떤 솔루션으로도 해결되지 않는 고도로 특화된 문제를 안고 있습니다.

이 패키지가 채택된다고 해서 “분산” 문제를 다루는 프로그래머가 자신의 문제 영역에 맞는 다른 솔루션을 검토하지 못하게 되거나, 검토하지 말라고 권하는 것은 아닙니다. 이 패키지를 포함하려는 의도는 로컬 동시성을 위한 입문 수준의 기능과, 그 동시성을 네트워크로 연결된 머신들로 넓히기 위한 기본 지원을 제공하는 것입니다. 두 기능은 긴밀하게 결합되어 있지 않으며, 실제로 pyprocessing 패키지는 MPI 등을 비롯한 다른 어떤 솔루션과도 함께 사용할 수 있습니다.

필요하다면 패키지의 로컬 동시성 기능을 네트워크 지원 및 공유 기능과 완전히 분리할 수도 있습니다. 그러나 심각한 우려나 이유가 없는 한, 이 PEP의 저자는 그러한 방식을 권장하지 않습니다.

성능 비교

모두가 알다시피 “거짓말, 새빨간 거짓말, 그리고 벤치마크”가 있습니다. 이 속도 비교는 pyprocessing 패키지의 성능을 보여 주기 위한 것이지만, 결코 포괄적이지 않으며 가능한 모든 사용 사례나 환경에 들어맞는 것도 아닙니다. 프로세스 포크가 느린 플랫폼에서는 특히 그렇습니다.

모든 벤치마크는 다음 환경에서 실행했습니다:

  • 4코어 Intel Xeon CPU @ 3.00GHz
  • 16 GB RAM
  • Gentoo Linux(커널 2.6.18.6)에서 컴파일한 Python 2.5.2
  • pyProcessing 0.52

이 벤치마크의 모든 코드는 http://jessenoller.com/code/bench-src.tgz 에서 내려받을 수 있습니다.

이 벤치마크의 기본 실행 방식은 run_benchmarks.py [6] 스크립트에 들어 있습니다. 이 스크립트는 대상 함수를 단일 스레드(선형), 다중 스레드(threading 사용), 다중 프로세스(pyprocessing 사용) 방식으로 실행하는 단순한 래퍼입니다. 반복 횟수는 고정하고 실행 루프 수나 스레드 수, 또는 둘 다를 늘려 가며 실행합니다.

run_benchmarks.py 스크립트는 각 함수를 100번 실행하고, timeit 모듈을 사용해 그 100번의 반복 중 가장 좋은 결과를 고릅니다.

먼저 워커를 생성하는 데 드는 오버헤드를 확인하기 위해 pass 문 하나로만 이루어진 (빈) 함수를 실행합니다.:

cmd: python run_benchmarks.py empty_func.py
Importing empty_func
Starting tests ...
non_threaded (1 iters)  0.000001 seconds
threaded (1 threads)    0.000796 seconds
processes (1 procs)     0.000714 seconds

non_threaded (2 iters)  0.000002 seconds
threaded (2 threads)    0.001963 seconds
processes (2 procs)     0.001466 seconds

non_threaded (4 iters)  0.000002 seconds
threaded (4 threads)    0.003986 seconds
processes (4 procs)     0.002701 seconds

non_threaded (8 iters)  0.000003 seconds
threaded (8 threads)    0.007990 seconds
processes (8 procs)     0.005512 seconds

보시다시피 pyprocessing 패키지로 프로세스를 포크하는 것이 스레드 버전의 코드를 구성한 뒤 실행하는 것보다 빠릅니다.

두 번째 테스트는 각 스레드 안에서 피보나치 수 50000개를 계산합니다(각 스레드는 격리되어 있으며 아무것도 공유하지 않습니다).:

cmd: python run_benchmarks.py fibonacci.py
Importing fibonacci
Starting tests ...
non_threaded (1 iters)  0.195548 seconds
threaded (1 threads)    0.197909 seconds
processes (1 procs)     0.201175 seconds

non_threaded (2 iters)  0.397540 seconds
threaded (2 threads)    0.397637 seconds
processes (2 procs)     0.204265 seconds

non_threaded (4 iters)  0.795333 seconds
threaded (4 threads)    0.797262 seconds
processes (4 procs)     0.206990 seconds

non_threaded (8 iters)  1.591680 seconds
threaded (8 threads)    1.596824 seconds
processes (8 procs)     0.417899 seconds

세 번째 테스트는 100000 미만의 모든 소수의 합을 계산하며, 역시 아무것도 공유하지 않습니다.:

cmd: run_benchmarks.py crunch_primes.py
Importing crunch_primes
Starting tests ...
non_threaded (1 iters)  0.495157 seconds
threaded (1 threads)    0.522320 seconds
processes (1 procs)     0.523757 seconds

non_threaded (2 iters)  1.052048 seconds
threaded (2 threads)    1.154726 seconds
processes (2 procs)     0.524603 seconds

non_threaded (4 iters)  2.104733 seconds
threaded (4 threads)    2.455215 seconds
processes (4 procs)     0.530688 seconds

non_threaded (8 iters)  4.217455 seconds
threaded (8 threads)    5.109192 seconds
processes (8 procs)     1.077939 seconds

두 번째와 세 번째 테스트가 순수한 수치 계산에 초점을 맞춘 이유는 현재의 스레딩 구현이 I/O 중심이 아닌 애플리케이션에 실제로 방해가 된다는 것을 보여 주기 위해서입니다. 물론 결과와 작업 단위를 조율하는 데 큐를 사용하도록 이 테스트들을 개선할 수도 있지만, 이 패키지와 핵심 processing.process 모듈의 성능을 보여 주는 데에는 필요하지 않습니다.

다음 테스트는 I/O 바운드 테스트입니다. 보통은 이런 경우에 threading 모듈 방식이 단일 스레드 방식보다 크게 개선되는 것을 볼 수 있습니다. 이 테스트에서 각 워커는 lorem.txt에 대한 디스크립터를 열고, 그 안에서 무작위 위치로 탐색하며 /dev/null에 줄을 씁니다.:

cmd: python run_benchmarks.py file_io.py
Importing file_io
Starting tests ...
non_threaded (1 iters)  0.057750 seconds
threaded (1 threads)    0.089992 seconds
processes (1 procs)     0.090817 seconds

non_threaded (2 iters)  0.180256 seconds
threaded (2 threads)    0.329961 seconds
processes (2 procs)     0.096683 seconds

non_threaded (4 iters)  0.370841 seconds
threaded (4 threads)    1.103678 seconds
processes (4 procs)     0.101535 seconds

non_threaded (8 iters)  0.749571 seconds
threaded (8 threads)    2.437204 seconds
processes (8 procs)     0.203438 seconds

보시다시피 이 I/O 작업에서도 여전히 pyprocessing이 다중 스레드를 사용하는 것보다 빠릅니다. 게다가 다중 스레드를 사용하는 것은 단일 스레드 실행 자체보다도 느립니다.

마지막으로 네트워크 I/O 성능을 보여 주기 위해 소켓 기반 테스트를 실행하겠습니다. 이 함수는 LAN에 있는 서버에서 URL 하나를 가져오는데, 이 URL은 tomcat의 단순한 오류 페이지입니다. 이 함수는 페이지를 100번 가져옵니다. 네트워크에는 다른 트래픽이 없으며, 10G 연결입니다.:

cmd: python run_benchmarks.py url_get.py
Importing url_get
Starting tests ...
non_threaded (1 iters)  0.124774 seconds
threaded (1 threads)    0.120478 seconds
processes (1 procs)     0.121404 seconds

non_threaded (2 iters)  0.239574 seconds
threaded (2 threads)    0.146138 seconds
processes (2 procs)     0.138366 seconds

non_threaded (4 iters)  0.479159 seconds
threaded (4 threads)    0.200985 seconds
processes (4 procs)     0.188847 seconds

non_threaded (8 iters)  0.960621 seconds
threaded (8 threads)    0.659298 seconds
processes (8 procs)     0.298625 seconds

드디어 스레드 성능이 단일 스레드 실행을 앞지르는 것을 볼 수 있지만, 워커 수를 늘리면 여전히 pyprocessing 패키지가 더 빠릅니다. 스레드나 워커를 한두 개로 유지하면 스레드와 pyprocessing의 실행 시간은 상당히 비슷합니다.

다만 한 가지 주목할 점은 객체 직렬화 때문에 pyprocessing 패키지의 Queue 구현에 암묵적인 오버헤드가 있다는 것입니다.

Alec Thomas는 기본 Queue 구현과 비교하여 이 오버헤드를 보여 주는 짧은 예제를 run_benchmarks.py 스크립트를 바탕으로 작성해 제공했습니다.:

cmd: run_bench_queue.py
non_threaded (1 iters)  0.010546 seconds
threaded (1 threads)    0.015164 seconds
processes (1 procs)     0.066167 seconds

non_threaded (2 iters)  0.020768 seconds
threaded (2 threads)    0.041635 seconds
processes (2 procs)     0.084270 seconds

non_threaded (4 iters)  0.041718 seconds
threaded (4 threads)    0.086394 seconds
processes (4 procs)     0.144176 seconds

non_threaded (8 iters)  0.083488 seconds
threaded (8 threads)    0.184254 seconds
processes (8 procs)     0.302999 seconds

추가 벤치마크는 pyprocessing 패키지 소스 배포판의 examples/ 디렉터리에서 찾을 수 있습니다. 이 예제들은 패키지 문서에 포함될 예정입니다.

유지 관리

pyprocessing 패키지의 작성자인 Richard M. Oudkerk가 Python SVN 내에서 이 패키지를 유지 관리하기로 동의했습니다. Jesse Noller도 패키지의 유지 관리와 문서화, 테스트를 돕겠다고 자원했습니다.

API 명명

이 패키지의 API는 python 2.x 기준 threading 모듈과 Queue 모듈의 API를 거의 그대로 따르도록 설계되었지만, 그 모듈들은 PEP 8을 준수하지 않습니다. 패키지를 “있는 그대로” 추가하여 PEP 8을 준수하지 않는 명명을 이어 가는 대신, 모든 API와 클래스 등의 이름을 PEP 8에 완전히 맞도록 바꾸기로 결정했습니다.

이 변경으로 threading 모듈 사용자가 이 패키지로 손쉽게 바꿔 끼우기는 어려워지지만, 특히 threading 모듈의 API 자체도 바뀔 예정이라는 점을 고려하면 저자들은 이를 받아들일 만한 부작용으로 봅니다.

트래커의 이슈 3042는 Python 2.6에서 threading 모듈에 현재 API와 PEP 8을 준수하는 API의 두 가지를 두자고 제안합니다. -3 옵션으로 실행하면 기존 Java 스타일 API가 곧 제거된다는 경고가 발생합니다.

Python 3000에서는 threading API가 PEP 8을 준수하게 되므로, multiprocessing 모듈과 threading 모듈의 API가 다시 일치하게 됩니다.

시기/일정

올해 나올 2.6 및 3.0 릴리스에 이 PEP를 넣기에는 시기가 늦었다는 우려도 일부 제기되었지만, 저자들과 다른 사람들 모두 이 패키지가 제공하는 기능이 포함에 따르는 위험보다 크다고 봅니다.

다만 Python 코어를 불안정하게 만들지 않아야 한다는 점을 고려하여, pyprocessing 코드를 Python 코어 “안으로” 옮기는 리팩터링 일부는 다음 2.x/3.x 릴리스까지 미룰 수 있습니다. 따라서 Python 코어에 대한 실제 위험은 아주 작으며, 대체로 패키지 자체에 한정됩니다.

미해결 문제

  • “기본값”으로 제공되는 원격 연결 기능이 없음을 확인하고, 필요하다면 원격 기능을 제공하는 클래스에 대해 원격 보안 메커니즘을 기본으로 활성화합니다.
  • API 일부(Queue 메서드 qsize(), task_done(), join())는 추가하거나, 제외한 이유를 파악하여 명확히 문서화해야 합니다.

해결된 문제

  • 패키지 단위 테스트가 동작하려면 roudkerk가 이슈 1683에 제출한 PyGILState 버그 패치를 적용해야 합니다.
  • 기존 문서를 ReST 형식으로 옮겨야 합니다.
  • ctypes 의존: pyprocessing 패키지는 ctypes에 의존하므로 ctypes를 지원하지 않는 플랫폼에서는 동작하지 않습니다. 이는 이 패키지의 제약이 아니라 ctypes의 제약입니다.
  • 완료: 최상위 패키지 이름을 “pyprocessing”에서 “multiprocessing”으로 바꿉니다.
  • 완료: 또한 프로세스 생성의 기본 동작 때문에 이 패키지를 IDLE 안에서 그대로 사용할 수 없다는 점에 유의하십시오. 이는 버그 수정이나 “setExecutable” 기능 개선으로 검토할 예정입니다.
  • 완료: 패키지가 Python 인터프리터 대신 현재 실행 파일 이름으로 프로세스를 생성하도록 기본 동작을 재정의하는 “multiprocessing.setExecutable()” 메서드를 추가합니다. Mark Hammond가 이를 위해 팩토리 스타일 인터페이스를 제안했다는 점에 유의하십시오 [7].

참고 문헌