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] 패키지를 Python 표준 라이브러리에 포함하고, 이름을 “multiprocessing”으로 변경할 것을 제안합니다.

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

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

패키지의 분산 기능도 유용하지만, 이 PEP의 주요 초점은 패키지의 핵심 스레딩 유사 API와 기능입니다.

근거

현재 CPython 인터프리터는 전역 인터프리터 잠금(Global Interpreter Lock, GIL)을 구현하고 있으며, 현재 계획된 Python 3000 또는 다른 버전에서의 작업 [2] 을 제외하면, 가까운 미래에도 GIL은 CPython 인터프리터 내에서 현재 상태로 유지될 것입니다. GIL 자체는 인터프리터와 확장 기능 기반을 위한 깔끔하고 유지 관리하기 쉬운 C 코드를 가능하게 하지만, 멀티코어 머신을 활용하는 Python 프로그래머에게는 자주 문제가 됩니다.

GIL 자체는 어느 시점에서든 인터프리터 내부에서 둘 이상의 스레드가 실행되는 것을 방지하므로, Python이 다중 프로세서 시스템의 이점을 활용할 수 있는 능력을 사실상 제거합니다.

pyprocessing 패키지는 GIL을 우회하는 방법을 제공하므로, 사용자가 프로그래밍 패러다임을 완전히 변경하지 않고도(즉, 스레드 프로그래밍을 다른 “동시성” 방식인 Twisted, Actors 등으로 바꾸지 않고도) CPython 내의 애플리케이션이 멀티코어 아키텍처를 활용할 수 있습니다.

Processing 패키지는 CPython에 잘 알려진 의미론과 손쉬운 확장성을 갖춘 “알려진 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 클래스를 통해 실행됩니다. 물론 API를 PEP 8 준수 형식으로 이름을 변경하면 사용자 애플리케이션 내부에서도 추가적인 이름 변경이 필요하지만, 그 변경은 사소합니다.

이러한 호환성 덕분에 대부분의 경우 코드를 약간만 변경하면 사용자 애플리케이션이 주어진 머신의 모든 코어와 프로세서를 병렬 실행에 활용할 수 있습니다. 많은 경우 pyprocessing 패키지는 I/O 바운드 프로그램에서 일반적인 스레딩 방식보다 훨씬 빠르기도 합니다. 이는 물론 pyprocessing 패키지가 최적화된 C 코드로 구현되어 있는 반면 threading 모듈은 그렇지 않다는 점을 고려한 것입니다.

“분산” 문제

이 패키지의 포함에 관한 Python-Dev 논의 [3] 에서는 이 PEP가 “분산” 문제를 해결하려는 의도라는 점에 대해 혼동이 있었으며, 이 패키지의 기능을 MPI 기반 통신 [4], CORBA 또는 기타 분산 객체 방식 [5] 과 같은 다른 해결책과 자주 비교했습니다.

“분산” 문제는 규모가 크고 다양합니다. 이 영역에서 작업하는 각 프로그래머는 자신이 선호하는 모듈이나 메서드에 대해 매우 강한 의견을 갖고 있거나, 기존의 어떤 해결책도 작동하지 않는 고도로 맞춤화된 문제를 갖고 있습니다.

이 패키지를 수용한다고 해서 “분산” 문제를 다루는 프로그래머가 자신의 문제 영역에 대한 다른 해결책을 검토하는 것이 배제되거나, 그러한 검토를 하지 말도록 권장되는 것은 아닙니다. 이 패키지를 포함하려는 의도는 로컬 동시성을 위한 입문 수준의 기능과 그러한 동시성을 머신 네트워크 전체로 확장하기 위한 기본 지원을 제공하는 것입니다. 두 기능은 긴밀하게 결합되어 있지는 않지만, pyprocessing 패키지는 실제로 MPI 등을 포함한 다른 해결책과 함께 사용할 수 있습니다.

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

성능 비교

우리 모두 알고 있듯이 “거짓말, 빌어먹을 거짓말, 그리고 벤치마크”가 있습니다. 이러한 속도 비교는 pyprocessing 패키지의 성능을 보여 주는 것을 목표로 하지만, 결코 모든 가능한 사용 사례나 환경에 포괄적으로 적용될 수 있는 것은 아닙니다. 특히 프로세스 포크 시간이 느린 플랫폼에서는 더욱 그렇습니다.

모든 벤치마크는 다음을 사용하여 실행했습니다:

  • 4코어 Intel Xeon CPU @ 3.00GHz입니다.
  • 16GB 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

테스트 2와 3에서 순수한 수치 연산에 집중한 이유는 현재의 스레딩 구현이 비 I/O 애플리케이션을 어떻게 저해하는지 보여 주기 위해서입니다. 물론 결과와 작업 청크를 조정하기 위해 큐를 사용하도록 이러한 테스트를 개선할 수 있지만, 패키지와 핵심 processing.process 모듈의 성능을 보여 주는 데에는 필요하지 않습니다.

다음 테스트는 I/O 바운드 테스트입니다. 일반적으로 이 경우 단일 스레드 방식에 비해 스레딩 모듈 방식이 크게 향상되는 것을 볼 수 있습니다. 이 경우 각 작업자는 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의 서버에서 tomcat의 단순한 오류 페이지인 URL을 가져옵니다. 해당 페이지를 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/ 디렉터리에서 찾을 수 있습니다. 이 예제들은 패키지 문서에 포함될 예정입니다.

유지 관리

Richard M. Oudkerk는 pyprocessing 패키지의 저자로서, Python SVN 내에서 해당 패키지를 유지 관리하는 데 동의하였습니다. Jesse Noller는 패키지 유지 관리·문서화 및 테스트를 함께 돕겠다고 자원하였습니다.

API 이름 규칙

이 패키지 API의 목표는 python 2.x 기준의 threading 및 Queue 모듈과 유사하게 설계하는 것이지만, 그 모듈들은 PEP 8을 준수하지 않습니다. 이 패키지를 “있는 그대로” 추가하여 PEP 8을 준수하지 않는 명명 방식을 그대로 이어가는 대신, 모든 API와 클래스 등의 이름을 완전히 PEP 8 준수 방식으로 변경하기로 결정하였습니다.

이 변경은 threading 모듈을 사용하던 사람들에게 손쉬운 대체 사용을 저해하지만, 특히 threading 모듈 자체의 API도 변경될 예정임을 감안하면 저자들이 보기에 이는 받아들일 만한 부작용입니다.

트래커의 이슈 3042에서는 Python 2.6에 대해 threading 모듈용 API를 두 가지, 즉 현재 API와 PEP 8 준수 API를 함께 제공할 것을 제안하고 있습니다. 기존의 java 스타일 API가 향후 제거될 것이라는 경고는 -3 옵션이 실행될 때 발생합니다.

Python 3000에서는 threading API가 PEP 8을 준수하게 되며, 이는 multiprocessing 모듈과 threading 모듈이 다시 서로 일치하는 API를 갖게 됨을 의미합니다.

시기/일정

올해의 2.6 및 3.0 릴리스에 대해 이 PEP의 시기/지연에 관한 우려가 일부 제기되었으나, 저자들과 다른 이들 모두 이 패키지가 제공하는 기능이 포함에 따른 위험을 능가한다고 판단하고 있습니다.

다만 Python-core를 불안정하게 만들지 않으려는 의도를 고려하여, pyprocessing 코드를 Python-core “안으로” 편입시키는 일부 리팩터링 작업은 다음 2.x/3.x 릴리스까지 보류할 수 있습니다. 이는 Python-core에 대한 실제 위험이 최소화되며, 대체로 해당 패키지 자체에 국한됨을 의미합니다.

미해결 이슈

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

해결된 이슈

  • roudkerk가 이슈 1683에 제출한 PyGILState 버그 패치를 적용해야만 패키지 단위 테스트가 동작합니다.
  • 기존 문서는 ReST 형식으로 옮겨야 합니다.
  • ctypes에 대한 의존: pyprocessing 패키지가 ctypes에 의존하는 탓에, ctypes를 지원하지 않는 플랫폼에서는 이 패키지가 동작하지 않습니다. 이는 이 패키지 자체의 제약이 아니라, ctypes의 제약입니다.
  • 완료: 최상위 패키지 이름을 “pyprocessing”에서 “multiprocessing”으로 변경하였습니다.
  • 완료: 또한 프로세스 생성의 기본 동작이 그대로는 IDLE 내에서의 사용과 호환되지 않는다는 점도 참고하시기 바랍니다. 이는 버그 수정이나 “setExecutable” 개선 방안으로 검토될 예정입니다.
  • 완료: 패키지가 Python 인터프리터가 아닌 현재 실행 파일 이름으로 프로세스를 생성하는 기본 동작을 재정의할 수 있도록 “multiprocessing.setExecutable()” 메서드를 추가하였습니다. Mark Hammond가 이에 대해 팩토리 방식의 인터페이스를 제안하였음을 참고하십시오 [7].

참고 자료