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

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

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

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

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

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

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



답변

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


답변

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

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

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


답변

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

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


답변

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에 의해 완전히 직렬화됩니다.
  • 프로세스가 병렬로 실행될 수 있습니다.


답변

다음은 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')


답변

글쎄, 대부분의 질문은 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

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


답변