파이썬의 다중 처리-부모 프로세스에서 forkserver 프로세스에 의해 상속되는 것은 무엇입니까?

Aug 15 2020

나는 사용하려고하는데 작업자 프로세스에서 forkserver만났습니다 NameError: name 'xxx' is not defined.

Python 3.6.4를 사용하고 있지만 문서는 다음과 같아야합니다. https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods 그것은 말한다 :

포크 서버 프로세스는 단일 스레드이므로 os.fork ()를 사용하는 것이 안전합니다. 불필요한 리소스는 상속되지 않습니다.

또한 다음과 같이 말합니다.

피클 / 언 피클보다 상속하는 것이 좋습니다.

spawn 또는 forkserver 시작 방법을 사용할 때 다중 처리의 많은 유형을 선택 가능해야 자식 프로세스가 사용할 수 있습니다 . 그러나 일반적으로 파이프 나 큐를 사용하여 공유 객체를 다른 프로세스로 보내는 것을 피해야합니다. 대신 다른 곳에서 생성 된 공유 리소스에 액세스해야하는 프로세스가 조상 프로세스에서 상속 할 수 있도록 프로그램을 정렬해야합니다.

내 작업자 프로세스가 작업해야하는 핵심 개체가 서버 프로세스에 상속되지 않고 작업자에게 전달되는 이유는 무엇입니까? 부모 프로세스에서 forkserver 프로세스에 의해 정확히 무엇이 상속되는지 궁금합니다.

내 코드는 다음과 같습니다.

import multiprocessing
import (a bunch of other modules)

def worker_func(nameList):
    global largeObject
    for item in nameList:
        # get some info from largeObject using item as index
        # do some calculation
        return [item, info]

if __name__ == '__main__':
    result = []
    largeObject # This is my large object, it's read-only and no modification will be made to it.
    nameList # Here is a list variable that I will need to get info for each item in it from the largeObject    
    ctx_in_main = multiprocessing.get_context('forkserver')
    print('Start parallel, using forking/spawning/?:', ctx_in_main.get_context())
    cores = ctx_in_main.cpu_count()
    with ctx_in_main.Pool(processes=4) as pool:
        for x in pool.imap_unordered(worker_func, nameList):
            result.append(x)

감사합니다!

베스트,

답변

1 alex_noname Aug 16 2020 at 19:32

이론

아래는 Bojan Nikolic 블로그 에서 발췌 한 것입니다.

최신 Python 버전 (Linux)은 별도의 프로세스를 시작하는 세 가지 방법을 제공합니다.

  1. Fork () -부모 프로세스를 처리하고 부모와 자식 모두에서 동일한 프로세스 이미지를 계속합니다. 이 방법은 빠르지 만 상위 상태가 복잡한 경우 잠재적으로 신뢰할 수 없습니다.

  2. 산란 자식 프로세스를, 즉, 포크 () - 보내고 다음 새 파이썬 프로세스와 프로세스 이미지를 대체 할 execv와. 이 방법은 신뢰할 수 있지만 프로세스 이미지가 새로 다시로드되기 때문에 느립니다.

  3. forkserver 메커니즘은 비교적 간단한 상태를 가진 별도의 Python 서버로 구성되며 새로운 프로세스가 필요할 때 fork ()-ed됩니다. 이 메서드는 Fork ()-ing의 속도와 좋은 안정성을 결합합니다 (fork되는 부모가 단순한 상태이기 때문에).

포크 서버

세 번째 방법 인 forkserver 가 아래에 설명되어 있습니다. 자식은 forkserver 상태의 복사본을 유지합니다. 이 상태는 상대적으로 단순하도록 의도되었지만 set_forkserver_preload()메서드를 통해 다중 프로세스 API를 통해이를 조정할 수 있습니다 .

연습

따라서 부모로부터 자식 프로세스가 simething을 상속 받도록하려면 forkserver 프로세스에서 로드하려고 시도 할 모듈 이름 목록을 설정 하는를 통해 forkserver 상태 에서 지정해야합니다 set_forkserver_preload(modules_names). 아래에 예를 들었습니다.

# inherited.py
large_obj = {"one": 1, "two": 2, "three": 3}
# main.py
import multiprocessing
import os
from time import sleep

from inherited import large_obj


def worker_func(key: str):
    print(os.getpid(), id(large_obj))
    sleep(1)
    return large_obj[key]


if __name__ == '__main__':
    result = []
    ctx_in_main = multiprocessing.get_context('forkserver')
    ctx_in_main.set_forkserver_preload(['inherited'])
    cores = ctx_in_main.cpu_count()
    with ctx_in_main.Pool(processes=cores) as pool:
        for x in pool.imap(worker_func, ["one", "two", "three"]):
            result.append(x)
    for res in result:
        print(res)

산출:

# The PIDs are different but the address is always the same
PID=18603, obj id=139913466185024
PID=18604, obj id=139913466185024
PID=18605, obj id=139913466185024

그리고 우리가 미리로드를 사용하지 않는다면

...
    ctx_in_main = multiprocessing.get_context('forkserver')
    # ctx_in_main.set_forkserver_preload(['inherited']) 
    cores = ctx_in_main.cpu_count()
...
# The PIDs are different, the addresses are different too
# (but sometimes they can coincide)
PID=19046, obj id=140011789067776
PID=19047, obj id=140011789030976
PID=19048, obj id=140011789030912
1 sgyzetrov Aug 17 2020 at 03:00

그래서 Alex와 고무적인 토론을 한 후에 나는 내 질문을 해결할 충분한 정보를 가지고 있다고 생각합니다. 상위 프로세스에서 forkserver 프로세스에 의해 정확히 상속되는 것은 무엇입니까?

기본적으로 서버 프로세스가 시작되면 메인 모듈을 가져오고 이전의 모든 if __name__ == '__main__'것이 실행됩니다. 그건 내 코드가없는 일을하는 이유 때문에,의 large_object에서 찾을 수 아무데도 server그 작업자 프로세스 과정과 모든 년에서 포크하는 server과정 .

Alex의 솔루션은 large_object이제 기본 및 서버 프로세스로 가져 오기 때문에 서버에서 분기 된 모든 작업자도 large_object. set_forkserver_preload(modules_names)모든 작업자 와 결합하면 내가 본 것과 같은 결과 large_object 를 얻을 수도 있습니다 . 사용 이유 forkserver는 Python 문서와 Bojan의 블로그에 명시 적으로 설명되어 있습니다.

프로그램이 시작되고 forkserver 시작 방법을 선택하면 서버 프로세스가 시작됩니다. 그때부터 새 프로세스가 필요할 때마다 상위 프로세스는 서버에 연결하여 새 프로세스를 포크하도록 요청합니다. 포크 서버 프로세스는 단일 스레드이므로 os.fork ()를 사용하는 것이 안전합니다. 불필요한 자원은 상속되지 않습니다 .

forkserver 메커니즘은 비교적 간단한 상태를 가진 별도의 Python 서버로 구성되며 새로운 프로세스가 필요할 때 fork ()-ed됩니다. 이 메서드는 Fork ()-ing의 속도와 좋은 안정성을 결합합니다 (fork되는 부모가 단순 상태이기 때문에) .

그래서 여기서는 더 안전한 측면에 있습니다.

당신이 사용하는 경우 보조 노트에, fork시작 방법으로 시스템 사용 COW-이 경우 모든 자식 프로세스가 부모 프로세스 메모리의 복사본 (또는 참조를 가져옵니다 때문에 당신은 수입 아무것도 필요하지 않습니다,하지만 copy-on-write나는 경우에, 저를 수정하시기 바랍니다 잘못된). 이 경우를 사용 global large_object하면 large_objectworker_func직접 액세스 할 수 있습니다.

forkserver내가 직면하고있는 문제는 메모리 오버 헤드 때문에 힘은 나를 위해 적절한 방법이 될 수 없습니다. 저 large_object를 처음으로 가져 오는 모든 작업 은 메모리를 많이 사용하므로 작업자 프로세스에 불필요한 리소스가 필요하지 않습니다.

inherited.pyAlex가 제안한대로 모든 계산을 직접 입력하면 두 번 실행 됩니다 (한 번 메인에서 모듈을 가져올 때 한 번, 서버가 모듈을 가져올 때 한 번; 작업자 프로세스가 생성 될 때 더 많을 수도 있습니까?). 작업자가 포크 할 수있는 단일 스레드 안전 프로세스를 원합니다. 그러나 작업자가 불필요한 리소스를 상속하지 large_object않고. 만 얻도록 노력하고 있기 때문에 작동하지 않습니다. 그리고 이들 계산 퍼팅 __main__에서하는 것은 inherited.py이제 프로세스 중에 주와 서버를 포함하여,이를 실행되지 않으므로 중 하나가 작동하지 않습니다.

따라서 결론적으로 작업자가 최소한의 리소스를 상속하도록하는 것이 목표라면 코드를 2로 나누고 calculation.py먼저를 피클 large_object하고 인터프리터를 종료 한 다음 새로운 것을 시작하여 피클을로드하는 것이 large_object좋습니다. 그런 다음 fork또는 forkserver.