스레딩 모듈과 다중 처리 모듈의 차이점은 무엇입니까?


145

특정 작업을 병렬로 실행하고 코드 속도를 높이기 위해 Python에서 threadingmultiprocessing모듈 을 사용하는 방법을 배우고 있습니다.

나는 threading.Thread()물체와 물체 의 차이점을 이해하기가 어렵습니다 (아마도 그것에 대한 이론적 배경이 없기 때문에) multiprocessing.Process().

또한 작업 대기열을 인스턴스화하는 방법과 작업 대기열 중 4 개만 병렬로 실행하는 방법과 다른 작업은 실행되기 전에 리소스가 해제 될 때까지 기다리는 방법이 명확하지 않습니다.

문서의 예는 명확하지만 그다지 완전하지는 않습니다. 작업을 조금 복잡하게 만들 자마자 많은 이상한 오류가 발생합니다 (예 : 절임 할 수없는 방법 등).

그렇다면 언제 threadingmultiprocessing모듈을 사용해야 합니까?

이 두 모듈의 개념과 복잡한 작업에 적절하게 사용하는 방법을 설명하는 몇 가지 리소스에 연결해 주시겠습니까?


더 많은 것이 있으며 Thread모듈 ( _threadpython 3.x에서 호출 됨)도 있습니다. 솔직히 말해서, 나 자신 ... 차이를 이해 적이
몰라

3
@Dunno : Thread/ _thread문서에서 명시 적으로 말했듯이 "저수준 기본 요소"입니다. 이를 사용하여 사용자 지정 동기화 개체를 빌드하고 스레드 트리의 결합 순서를 제어 할 수 있습니다. 왜 사용해야하는지 상상할 수 없다면 사용하지 말고 threading.
abarnert

답변:


267

Giulio Franco가 말하는 것은 일반적으로 멀티 스레딩 대 멀티 프로세싱 대해 사실입니다 .

그러나 Python * 에는 추가 문제가 있습니다. 동일한 프로세스에있는 두 개의 스레드가 동시에 Python 코드를 실행하지 못하도록하는 Global Interpreter Lock이 있습니다. 즉, 8 개의 코어가 있고 8 개의 스레드를 사용하도록 코드를 변경하면 800 % CPU를 사용하고 8 배 더 빠르게 실행할 수 없습니다. 동일한 100 % CPU를 사용하고 동일한 속도로 실행됩니다. (실제로는 공유 데이터가 없더라도 스레딩으로 인한 추가 오버 헤드가 있기 때문에 약간 느리게 실행되지만 지금은 무시합니다.)

이에 대한 예외가 있습니다. 코드의 무거운 계산이 실제로 Python에서 발생하지 않지만 numpy 앱과 같이 적절한 GIL 처리를 수행하는 사용자 지정 C 코드가있는 일부 라이브러리에서는 스레딩을 통해 예상되는 성능 이점을 얻을 수 있습니다. 실행하고 대기하는 일부 하위 프로세스에서 무거운 계산을 수행하는 경우에도 마찬가지입니다.

더 중요한 것은 이것이 중요하지 않은 경우가 있다는 것입니다. 예를 들어 네트워크 서버는 네트워크에서 패킷을 읽는 데 대부분의 시간을 소비하고 GUI 앱은 사용자 이벤트를 기다리는 데 대부분의 시간을 소비합니다. 네트워크 서버 또는 GUI 앱에서 스레드를 사용하는 한 가지 이유는 주 스레드가 네트워크 패킷 또는 GUI 이벤트를 계속 서비스하는 것을 중지하지 않고 장기 실행 "백그라운드 작업"을 수행 할 수 있도록하기 위해서입니다. 그리고 그것은 파이썬 스레드에서 잘 작동합니다. (기술적 인 측면에서 이것은 파이썬 스레드가 코어 병렬성을 제공하지 않더라도 동시성을 제공함을 의미합니다.)

그러나 순수 Python으로 CPU 바인딩 된 프로그램을 작성하는 경우 더 많은 스레드를 사용하는 것은 일반적으로 도움이되지 않습니다.

별도의 프로세스를 사용하는 것은 각 프로세스가 자체적으로 별도의 GIL을 가지고 있기 때문에 GIL에 그러한 문제가 없습니다. 물론 다른 언어에서와 마찬가지로 스레드와 프로세스 간에는 동일한 트레이드 오프가 있습니다. 스레드간에 데이터를 공유하는 것보다 프로세스간에 데이터를 공유하는 것이 더 어렵고 비용이 많이 들며, 엄청난 수의 프로세스를 실행하거나 생성 및 삭제하는 데 비용이 많이들 수 있습니다. 하지만 GIL은 예를 들어 C 또는 Java에 대해 사실이 아닌 방식으로 프로세스에 대한 균형에 크게 무게를 둡니다. 따라서 C 또는 Java에서보다 Python에서 훨씬 더 자주 다중 처리를 사용하게됩니다.


한편, 파이썬의 "배터리 포함"철학은 좋은 소식을 가져다줍니다. 한 줄의 변경으로 스레드와 프로세스 사이를 앞뒤로 전환 할 수있는 코드를 작성하는 것은 매우 쉽습니다.

입력 및 출력을 제외하고 다른 작업 (또는 기본 프로그램)과 공유하지 않는 자체 포함 된 "작업"측면에서 코드를 디자인하는 경우 concurrent.futures라이브러리를 사용하여 다음과 같이 스레드 풀을 중심으로 코드를 작성할 수 있습니다.

with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
    executor.submit(job, argument)
    executor.map(some_function, collection_of_independent_things)
    # ...

작업의 결과를 가져 와서 추가 작업에 전달할 수도 있고, 실행 순서 나 완료 순서 등을 기다릴 수도 있습니다. Future자세한 내용 은 개체 섹션 을 참조하십시오.

이제 프로그램이 100 % CPU를 지속적으로 사용하고 스레드를 더 추가하면 속도가 느려지는 것으로 밝혀지면 GIL 문제가 발생하므로 프로세스로 전환해야합니다. 당신이해야 할 일은 첫 번째 줄을 변경하는 것입니다.

with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:

유일한주의 사항은 작업의 인수와 반환 값이 피클 가능해야한다는 것입니다 (피클하는 데 너무 많은 시간이나 메모리가 걸리지 않아야 함). 일반적으로 이것은 문제가되지 않지만 때로는 문제가됩니다.


하지만 당신의 직업이 자립 할 수 없다면 어떨까요? 메시지 를 서로 전달 하는 작업의 관점에서 코드를 디자인 할 수 있다면 여전히 매우 쉽습니다. 풀 을 사용 threading.Thread하거나 multiprocessing.Process대신 풀 을 사용해야 할 수도 있습니다. 그리고 명시 적으로 queue.Queue또는 multiprocessing.Queue객체 를 생성해야합니다 . (파이프, 소켓, 무리가있는 파일 등 다른 많은 옵션이 있지만 요점은 Executor의 자동 마법이 불충분하면 수동으로 무언가 를해야 한다는 것 입니다.)

하지만 메시지 전달에 의존 할 수 없다면 어떨까요? 동일한 구조를 변경하고 서로의 변경 사항을 확인하기 위해 두 가지 작업이 필요한 경우 어떻게해야합니까? 이 경우 수동 동기화 (잠금, 세마포, 조건 등)를 수행해야하며 프로세스를 사용하려면 명시 적 공유 메모리 개체를 부팅해야합니다. 이것은 멀티 스레딩 (또는 멀티 프로세싱)이 어려워 질 때입니다. 피할 수 있다면 좋습니다. 할 수 없다면 누군가가 대답에 넣을 수있는 것보다 더 많이 읽어야 할 것입니다.


댓글에서 Python에서 스레드와 프로세스의 차이점을 알고 싶었습니다. 정말로, Giulio Franco의 답변과 저와 우리의 모든 링크를 읽으면 모든 것을 다루어야하지만 요약은 확실히 유용 할 것입니다.

  1. 스레드는 기본적으로 데이터를 공유합니다. 프로세스는 그렇지 않습니다.
  2. (1)의 결과로 프로세스간에 데이터를 전송하려면 일반적으로 데이터를 피클 링 및 언 피클 링해야합니다. **
  3. (1)의 또 다른 결과로, 프로세스간에 데이터를 직접 공유하려면 일반적으로 값, 배열 및 ctypes유형 과 같은 저수준 형식으로 데이터를 넣어야합니다 .
  4. 프로세스에는 GIL이 적용되지 않습니다.
  5. 일부 플랫폼 (주로 Windows)에서는 프로세스를 만들고 제거하는 데 훨씬 많은 비용이 듭니다.
  6. 프로세스에 대한 몇 가지 추가 제한 사항이 있으며 그 중 일부는 플랫폼마다 다릅니다. 자세한 내용은 프로그래밍 지침 을 참조하십시오.
  7. threading모듈의 일부 기능이없는 multiprocessing모듈을. ( multiprocessing.dummy쓰레드 위에 누락 된 API의 대부분을 가져 오는 데 사용할 수 있습니다 . 또는 concurrent.futures걱정하지 않고 같은 상위 수준 모듈을 사용할 수 있습니다 .)

* 실제로이 문제가있는 언어는 Python이 아니라 해당 언어의 "표준"구현 인 CPython입니다. 일부 다른 구현에는 Jython과 같은 GIL이 없습니다.

** 대부분의 비 Windows 플랫폼에서 수행 할 수있는 다중 처리 를 위해 포크 시작 방법을 사용하는 경우 각 자식 프로세스는 자식이 시작될 때 부모가 가지고 있던 모든 리소스를 가져옵니다. 이는 자식에게 데이터를 전달하는 또 다른 방법 일 수 있습니다.


감사합니다.하지만 모든 것을 이해했는지 잘 모르겠습니다. 어쨌든 나는 학습 목적으로 조금 시도하고 있는데, 스레드를 순진하게 사용하면 코드 속도가 절반으로 줄어들었기 때문입니다 (동시에 1000 개 이상의 스레드를 시작하여 각각 외부 앱을 호출합니다 .. 하지만 속도가 2 배 증가합니다). 스레드를 현명하게 관리하면 코드 속도가 정말 향상 될 것
같습니다

3
@LucaCerone : 아, 코드가 외부 프로그램을 기다리는 데 대부분의 시간을 소비한다면 예, 스레딩의 이점을 누릴 수 있습니다. 좋은 지적. 그것을 설명하기 위해 대답을 편집하겠습니다.
abarnert

3
@LucaCerone : 한편, 어떤 부분을 이해하지 못합니까? 당신이 시작하는 지식의 수준을 모르면 좋은 답을 쓰기가 어렵지만 ... 약간의 피드백을 통해 우리는 당신과 미래의 독자들에게도 도움이되는 무언가를 생각 해낼 수있을 것입니다.
abarnert

3
@LucaCerone 여기서 멀티 프로세싱에 대한 PEP를 읽어야합니다 . 스레드 대 다중 처리의 타이밍과 예를 제공합니다.
mr2ert 2013-08-07

1
@LucaCerone : 메서드가 바인딩 된 개체에 복잡한 상태가없는 경우 피클 링 문제에 대한 가장 간단한 해결 방법은 개체를 생성하고 해당 메서드를 호출하는 멍청한 래퍼 함수를 ​​작성하는 것입니다. 이 경우 않습니다 (; 예쁜 쉽게하는 복잡한 상태를 가지고, 당신은 아마 picklable해야 할 pickle문서가 그것을 설명), 그리고 최악의 멍청한 래퍼입니다 def wrapper(obj, *args): return obj.wrapper(*args).
abarnert

33

단일 프로세스에 여러 스레드가 존재할 수 있습니다. 동일한 프로세스에 속하는 스레드는 동일한 메모리 영역을 공유합니다 (매우 동일한 변수에서 읽고 쓸 수 있으며 서로 간섭 할 수 있음). 반대로, 서로 다른 프로세스는 서로 다른 메모리 영역에 있으며 각 프로세스에는 자체 변수가 있습니다. 통신을 위해 프로세스는 다른 채널 (파일, 파이프 또는 소켓)을 사용해야합니다.

계산을 병렬화하려면 스레드가 동일한 메모리에서 협력하기를 원하기 때문에 멀티 스레딩이 필요할 것입니다.

성능에 대해 말하자면 스레드는 프로세스보다 생성하고 관리하는 것이 더 빠르며 (OS가 완전히 새로운 가상 메모리 영역을 할당 할 필요가 없기 때문에) 스레드 간 통신은 일반적으로 프로세스 간 통신보다 빠릅니다. 그러나 스레드는 프로그래밍하기가 더 어렵습니다. 스레드는 서로 간섭 할 수 있고 서로의 메모리에 쓸 수 있지만 이것이 발생하는 방식이 항상 분명하지는 않습니다 (주로 명령어 재정렬 및 ​​메모리 캐싱과 같은 여러 요인으로 인해), 따라서 액세스를 제어하려면 동기화 기본 요소가 필요합니다. 변수에.


12
이것은 GIL에 대한 매우 중요한 정보가 누락되어 오해의 소지가 있습니다.
abarnert

1
@ mr2ert : 네, 아주 중요한 정보입니다. :)하지만 그것보다 조금 더 복잡해서 제가 별도의 답변을 썼습니다.
abarnert 2013-08-07

2
나는 @abarnert가 옳다고 언급했다고 생각했고 여기에 대답하면서 GIL을 잊어 버렸습니다. 따라서이 답변은 틀렸으므로 찬성해서는 안됩니다.
Giulio Franco

6
이 답변은 여전히 ​​Python threadingmultiprocessing.
Antti Haapala

모든 프로세스에 대해 GIL이 있다는 것을 읽었습니다. 그러나 모든 프로세스가 동일한 파이썬 인터프리터를 사용합니까 아니면 스레드마다 별도의 인터프리터가 있습니까?
변수

4

이 링크가 귀하의 질문에 우아한 방식으로 대답 한다고 생각 합니다 .

간단히 말해서, 하위 문제 중 하나가 다른 문제가 완료 될 때까지 기다려야하는 경우 멀티 스레딩이 좋습니다 (예 : I / O 무거운 작업에서). 반대로 하위 문제가 실제로 동시에 발생할 수 있다면 다중 처리가 제안됩니다. 그러나 코어 수보다 더 많은 프로세스를 생성하지 않습니다.


4

Python 문서 인용문

Process vs Threads 및 GIL에 대한 주요 Python 문서 인용문을 다음에서 강조했습니다. CPython의 GIL (Global Interpreter Lock)이란?

프로세스 대 스레드 실험

차이점을 더 구체적으로 보여주기 위해 약간의 벤치마킹을했습니다.

벤치 마크에서는 8 개의 하이퍼 스레드 CPU 에서 다양한 수의 스레드에 대해 CPU 및 IO 바인딩 작업을 시간을 정했습니다 . 스레드 당 제공되는 작업은 항상 동일하므로 스레드가 많을수록 전체 작업이 더 많이 공급됩니다.

결과는 다음과 같습니다.

여기에 이미지 설명 입력

데이터 플로팅 .

결론 :

  • CPU 바운드 작업의 경우 다중 처리가 항상 더 빠릅니다. 아마도 GIL 때문일 것입니다.

  • IO 바인딩 작업의 경우. 둘 다 정확히 같은 속도

  • 스레드는 8 하이퍼 스레드 머신을 사용하고 있기 때문에 예상되는 8x 대신 약 4x까지만 확장됩니다.

    예상되는 8 배 속도 향상에 도달하는 C POSIX CPU 바인딩 작업과 대조됩니다. 시간 (1)의 출력에서 ​​'실제', '사용자'및 'sys'는 무엇을 의미합니까?

    TODO : 그 이유를 모르겠습니다. 다른 Python 비 효율성이 작용해야합니다.

테스트 코드 :

#!/usr/bin/env python3

import multiprocessing
import threading
import time
import sys

def cpu_func(result, niters):
    '''
    A useless CPU bound function.
    '''
    for i in range(niters):
        result = (result * result * i + 2 * result * i * i + 3) % 10000000
    return result

class CpuThread(threading.Thread):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class CpuProcess(multiprocessing.Process):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class IoThread(threading.Thread):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

class IoProcess(multiprocessing.Process):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

if __name__ == '__main__':
    cpu_n_iters = int(sys.argv[1])
    sleep = 1
    cpu_count = multiprocessing.cpu_count()
    input_params = [
        (CpuThread, cpu_n_iters),
        (CpuProcess, cpu_n_iters),
        (IoThread, sleep),
        (IoProcess, sleep),
    ]
    header = ['nthreads']
    for thread_class, _ in input_params:
        header.append(thread_class.__name__)
    print(' '.join(header))
    for nthreads in range(1, 2 * cpu_count):
        results = [nthreads]
        for thread_class, work_size in input_params:
            start_time = time.time()
            threads = []
            for i in range(nthreads):
                thread = thread_class(work_size)
                threads.append(thread)
                thread.start()
            for i, thread in enumerate(threads):
                thread.join()
            results.append(time.time() - start_time)
        print(' '.join('{:.6e}'.format(result) for result in results))

GitHub 업스트림 + 동일한 디렉토리에 코드 플로팅 .

CPU : Intel Core i7-7820HQ CPU (4 코어 / 8 스레드), RAM : 2x Samsung M471A2K43BB1-CRC (2x 16GiB), SSD : Samsung MZVLB512HAJQ- 000L7 (3,000MB / s).

주어진 시간에 실행중인 스레드 시각화

이 게시물 https://rohanvarma.me/GIL/는 스레드가로 예정되어 때마다 콜백을 실행할 수 있음을 가르쳐 target=의 인수threading.Thread 및에 대해 동일한 multiprocessing.Process.

이를 통해 매번 실행되는 스레드를 정확하게 볼 수 있습니다. 이 작업이 완료되면 다음과 같은 내용이 표시됩니다 (이 특정 그래프를 만들었습니다).

            +--------------------------------------+
            + Active threads / processes           +
+-----------+--------------------------------------+
|Thread   1 |********     ************             |
|         2 |        *****            *************|
+-----------+--------------------------------------+
|Process  1 |***  ************** ******  ****      |
|         2 |** **** ****** ** ********* **********|
+-----------+--------------------------------------+
            + Time -->                             +
            +--------------------------------------+

다음과 같이 표시됩니다.

  • 스레드는 GIL에 의해 완전히 직렬화됩니다.
  • 프로세스가 병렬로 실행될 수 있습니다.

1

다음은 IO 바인딩 시나리오에서 스레딩이 다중 처리보다 더 성능이 좋다는 개념에 의문을 제기하는 Python 2.6.x의 성능 데이터입니다. 이 결과는 40 프로세서 IBM System x3650 M4 BD에서 가져온 것입니다.

IO-Bound Processing : 프로세스 풀이 스레드 풀보다 성능이 뛰어남

>>> do_work(50, 300, 'thread','fileio')
do_work function took 455.752 ms

>>> do_work(50, 300, 'process','fileio')
do_work function took 319.279 ms

CPU 바운드 처리 : 스레드 풀보다 성능이 우수한 프로세스 풀

>>> do_work(50, 2000, 'thread','square')
do_work function took 338.309 ms

>>> do_work(50, 2000, 'process','square')
do_work function took 287.488 ms

이것은 엄격한 테스트는 아니지만 멀티 프로세싱이 스레딩에 비해 완전히 성능이 떨어지는 것은 아니라고 말합니다.

위 테스트를 위해 대화 형 Python 콘솔에서 사용되는 코드

from multiprocessing import Pool
from multiprocessing.pool import ThreadPool
import time
import sys
import os
from glob import glob

text_for_test = str(range(1,100000))

def fileio(i):
 try :
  os.remove(glob('./test/test-*'))
 except : 
  pass
 f=open('./test/test-'+str(i),'a')
 f.write(text_for_test)
 f.close()
 f=open('./test/test-'+str(i),'r')
 text = f.read()
 f.close()


def square(i):
 return i*i

def timing(f):
 def wrap(*args):
  time1 = time.time()
  ret = f(*args)
  time2 = time.time()
  print '%s function took %0.3f ms' % (f.func_name, (time2-time1)*1000.0)
  return ret
 return wrap

result = None

@timing
def do_work(process_count, items, process_type, method) :
 pool = None
 if process_type == 'process' :
  pool = Pool(processes=process_count)
 else :
  pool = ThreadPool(processes=process_count)
 if method == 'square' : 
  multiple_results = [pool.apply_async(square,(a,)) for a in range(1,items)]
  result = [res.get()  for res in multiple_results]
 else :
  multiple_results = [pool.apply_async(fileio,(a,)) for a in range(1,items)]
  result = [res.get()  for res in multiple_results]


do_work(50, 300, 'thread','fileio')
do_work(50, 300, 'process','fileio')

do_work(50, 2000, 'thread','square')
do_work(50, 2000, 'process','square')

나는 당신의 코드를 사용했고 ( 글롭 부분을 제거했다 ) Python 2.6.6에서 흥미로운 결과를 발견했다 :>>> do_work(50, 300, 'thread', 'fileio') --> 237.557 ms >>> do_work(50, 300, 'process', 'fileio') --> 323.963 ms >>> do_work(50, 2000, 'thread', 'square') --> 232.082 ms >>> do_work(50, 2000, 'process', 'square') --> 282.785 ms
Alan Garrido

-5

글쎄, 대부분의 질문은 Giulio Franco가 대답합니다. 소비자-생산자 문제에 대해 더 자세히 설명하겠습니다.이 문제는 멀티 스레드 앱 사용에 대한 솔루션에 대한 올바른 방향을 제시 할 것입니다.

fill_count = Semaphore(0) # items produced
empty_count = Semaphore(BUFFER_SIZE) # remaining space
buffer = Buffer()

def producer(fill_count, empty_count, buffer):
    while True:
        item = produceItem()
        empty_count.down();
        buffer.push(item)
        fill_count.up()

def consumer(fill_count, empty_count, buffer):
    while True:
        fill_count.down()
        item = buffer.pop()
        empty_count.up()
        consume_item(item)

다음에서 동기화 기본 요소에 대해 자세히 읽을 수 있습니다.

 http://linux.die.net/man/7/sem_overview
 http://docs.python.org/2/library/threading.html

의사 코드는 위에 있습니다. 더 많은 참조를 얻으려면 생산자-소비자 문제를 검색해야한다고 생각합니다.


이노삼 미안하지만이게 C ++ 같아요? :) 링크를 주셔서 감사합니다
lucacerone

사실, 멀티 프로세싱과 멀티 스레딩의 아이디어는 언어 독립적입니다. 솔루션은 위의 코드와 유사합니다.
innosam 2013-08-07

2
이것은 C ++가 아닙니다. 의사 코드 (또는 C와 유사한 구문을 사용하는 대부분 동적 형식의 언어에 대한 코드입니다. 즉, Python 사용자를 교육하기 위해 Python과 유사한 의사 코드를 작성하는 것이 더 유용하다고 생각합니다. 특히 Python과 유사한 의사 코드가 실행 가능한 코드로 판명되거나 적어도 그에 가깝습니다. C와 유사한 의사 코드에는 거의 적용되지 않습니다…)
abarnert

파이썬과 유사한 의사 코드로 다시 작성했습니다 (전역 객체를 사용하는 대신 OO를 사용하고 매개 변수를 전달 함). 상황이 명확하지 않다고 생각되면 자유롭게 되 돌리십시오.
abarnert

또한 Python stdlib에는 이러한 모든 세부 정보를 래핑 하는 동기화 된 큐가 내장되어 있으며 스레드 및 프로세스 풀 API는 더 많은 것을 추상화합니다. 동기화 된 대기열이 내부적으로 어떻게 작동하는지 이해하는 것은 분명 가치가 있지만 직접 작성할 필요는 거의 없습니다.
abarnert 2013 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.