파이썬의 멀티 프로세싱 풀과 키보드 인터럽트


136

파이썬의 다중 처리 풀로 KeyboardInterrupt 이벤트를 어떻게 처리 할 수 ​​있습니까? 다음은 간단한 예입니다.

from multiprocessing import Pool
from time import sleep
from sys import exit

def slowly_square(i):
    sleep(1)
    return i*i

def go():
    pool = Pool(8)
    try:
        results = pool.map(slowly_square, range(40))
    except KeyboardInterrupt:
        # **** THIS PART NEVER EXECUTES. ****
        pool.terminate()
        print "You cancelled the program!"
        sys.exit(1)
    print "\nFinally, here are the results: ", results

if __name__ == "__main__":
    go()

위의 코드를 실행할 KeyboardInterrupt때을 누르면 ^C프로세스 가 발생 하지만 프로세스는 단순히 그 시점에서 중단되며 외부에서 종료해야합니다.

^C언제든지 누를 수 있고 모든 프로세스가 정상적으로 종료 되도록하고 싶습니다 .


psutil을 사용하여 문제를 해결했습니다. 여기에서 솔루션을 볼 수 있습니다. stackoverflow.com/questions/32160054/…
Tiago Albineli Motta

답변:


137

이것은 파이썬 버그입니다. threading.Condition.wait ()에서 조건을 기다리는 경우 KeyboardInterrupt가 전송되지 않습니다. 재현 :

import threading
cond = threading.Condition(threading.Lock())
cond.acquire()
cond.wait(None)
print "done"

KeyboardInterrupt 예외는 wait ()가 리턴 될 때까지 전달되지 않으며 리턴되지 않으므로 인터럽트가 발생하지 않습니다. KeyboardInterrupt는 거의 확실히 조건 대기를 중단해야합니다.

타임 아웃이 지정된 경우에는 이런 일이 발생하지 않습니다. cond.wait (1)은 즉시 인터럽트를 수신합니다. 따라서 해결 방법은 시간 초과를 지정하는 것입니다. 그렇게하려면 교체

    results = pool.map(slowly_square, range(40))

    results = pool.map_async(slowly_square, range(40)).get(9999999)

또는 유사합니다.


3
공식 파이썬 추적기의 버그입니까? 검색하는 데 문제가 있지만 최상의 검색어를 사용하지 않는 것 같습니다.
Joseph Garvin

18
이 버그는 [Issue 8296] [1]으로 제기되었습니다. [1] : bugs.python.org/issue8296
Andrey Vlasovskikh

1
다음은 동일한 방식으로 pool.imap ()를 수정하여 imap을 반복 할 때 Ctrl-C를 가능하게하는 해킹입니다. 예외를 포착하고 pool.terminate ()를 호출하면 프로그램이 종료됩니다. gist.github.com/626518
Alexander Ljungberg

6
이것은 문제를 해결하지 못합니다. 때로는 Control + C를 눌렀을 때 예상되는 동작을 얻습니다. 왜 그런지 모르겠지만 어쩌면 아마도 프로세스 중 하나가 KeyboardInterrupt를 임의로 수신하고 부모 프로세스가 프로세스를 잡는 프로세스 인 경우에만 올바른 동작을 얻습니다.
라이언 C. 톰슨

6
Windows의 Python 3.6.1에서는 작동하지 않습니다. Ctrl-C를 수행하면 스택 추적 및 기타 가비지가 발생합니다. 실제로이 스레드에서 시도한 솔루션 중 아무것도 작동하지 않는 것 같습니다.
szx

56

내가 최근에 찾은 것 중에서 가장 좋은 해결책은 작업자 프로세스를 설정하여 SIGINT를 완전히 무시하고 모든 정리 코드를 부모 프로세스로 제한하는 것입니다. 이는 유휴 및 바쁜 작업자 프로세스 모두에 대한 문제를 해결하며 자식 프로세스에서 오류 처리 코드가 필요하지 않습니다.

import signal

...

def init_worker():
    signal.signal(signal.SIGINT, signal.SIG_IGN)

...

def main()
    pool = multiprocessing.Pool(size, init_worker)

    ...

    except KeyboardInterrupt:
        pool.terminate()
        pool.join()

설명 및 전체 예제 코드는 각각 http://noswap.com/blog/python-multiprocessing-keyboardinterrupt/http://github.com/jreese/multiprocessing-keyboardinterrupt 에서 찾을 수 있습니다 .


4
안녕 존. 귀하의 솔루션은 불행히도 복잡한 솔루션과 같은 것을 달성하지 못합니다. time.sleep(10)기본 프로세스에서 숨겨져 있습니다. 해당 절전 모드를 제거했거나 작업이 완료되도록 보장하기 위해 프로세스가 풀에 참여하려고 시도 할 때까지 대기하는 경우 여전히 주요 프로세스와 동일한 문제가 발생합니다. 폴링 join작업을 기다리는 동안 KeyboardInterrupt를받지 못했습니다 .
bboe

프로덕션에서이 코드를 사용한 경우 time.sleep ()은 각 자식 프로세스의 상태를 확인한 다음 필요한 경우 지연에 따라 특정 프로세스를 다시 시작하는 루프의 일부였습니다. 모든 프로세스가 완료되기를 기다리는 join () 대신 프로세스를 개별적으로 확인하여 마스터 프로세스가 응답 상태를 유지하도록합니다.
John Reese

2
따라서 조인이 아닌 다른 방법을 통해 프로세스 완료를 폴링 한 것은 더 바쁜 대기 (수표 사이에 작은 수면이있을 수 있음)입니까? 이 경우 블로그 게시물에이 코드를 포함시키는 것이 좋습니다. 가입하기 전에 모든 작업자가 완료되었음을 보증 할 수 있습니다.
bboe

4
작동하지 않습니다. 어린이들만 신호를받습니다. 부모는 결코 그것을 pool.terminate()받지 못 하므로 결코 처형되지 않습니다. 어린이들이 그 신호를 무시하도록하는 것은 아무것도 달성하지 못한다. @Glenn의 답변은 문제를 해결합니다.
Cerin

1
내 버전은 gist.github.com/admackin/003dd646e5fadee8b8d6에 있습니다 . .join()인터럽트를 제외하고는 호출하지 않습니다. .apply_async()사용 AsyncResult.ready()준비 결과를 수동으로 확인하여 준비가 완료되었음을 의미합니다.
Andy MacKinlay

29

어떤 이유로 기본 Exception클래스 에서 상속 된 예외 만 정상적으로 처리됩니다. 이 문제를 해결하기 위해 다음 KeyboardInterrupt과 같이 Exception인스턴스를 다시 올릴 수 있습니다 .

from multiprocessing import Pool
import time

class KeyboardInterruptError(Exception): pass

def f(x):
    try:
        time.sleep(x)
        return x
    except KeyboardInterrupt:
        raise KeyboardInterruptError()

def main():
    p = Pool(processes=4)
    try:
        print 'starting the pool map'
        print p.map(f, range(10))
        p.close()
        print 'pool map complete'
    except KeyboardInterrupt:
        print 'got ^C while pool mapping, terminating the pool'
        p.terminate()
        print 'pool is terminated'
    except Exception, e:
        print 'got exception: %r, terminating the pool' % (e,)
        p.terminate()
        print 'pool is terminated'
    finally:
        print 'joining pool processes'
        p.join()
        print 'join complete'
    print 'the end'

if __name__ == '__main__':
    main()

일반적으로 다음과 같은 결과가 나타납니다.

staring the pool map
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
pool map complete
joining pool processes
join complete
the end

따라서를 누르면 ^C다음을 얻을 수 있습니다.

staring the pool map
got ^C while pool mapping, terminating the pool
pool is terminated
joining pool processes
join complete
the end

2
이것은 완전한 솔루션이 아닌 것 같습니다. 자체 IPC 데이터 교환을 수행하는 KeyboardInterrupt동안 a 가 도착 하면 (분명히) 활성화되지 않습니다. multiprocessingtry..catch
Andrey Vlasovskikh 2016

당신은 대체 할 수 raise KeyboardInterruptError로모그래퍼 return. KeyboardInterrupt가 수신 되 자마자 자식 프로세스가 종료되도록해야합니다. 반환 값은 무시되는 것처럼 보이지만 main여전히 KeyboardInterrupt가 수신됩니다.
Bernhard

8

일반적으로이 간단한 구조는 작동 Ctrl- C수영장에 :

def signal_handle(_signal, frame):
    print "Stopping the Jobs."

signal.signal(signal.SIGINT, signal_handle)

몇 가지 유사한 게시물에 언급 된 바와 같이 :

try-except없이 파이썬에서 키보드 인터럽트 캡처


1
이는 각 작업자 프로세스에서도 수행되어야하며 멀티 프로세싱 라이브러리가 초기화되는 동안 KeyboardInterrupt가 발생하면 여전히 실패 할 수 있습니다.
MarioVilas

7

투표 된 답변은 핵심 문제를 다루지는 않지만 유사한 부작용을 해결합니다.

멀티 프로세싱 라이브러리의 저자 인 Jesse Noller multiprocessing.Pool는 오래된 블로그 게시물 에서 CTRL + C를 올바르게 처리하는 방법을 설명합니다 .

import signal
from multiprocessing import Pool


def initializer():
    """Ignore CTRL+C in the worker process."""
    signal.signal(signal.SIGINT, signal.SIG_IGN)


pool = Pool(initializer=initializer)

try:
    pool.map(perform_download, dowloads)
except KeyboardInterrupt:
    pool.terminate()
    pool.join()

ProcessPoolExecutor에도 동일한 문제가 있음을 발견했습니다. (가) 단지 내가 찾을 수 있었다 해결하는 전화를했다 os.setpgrp()향후 내부에서
portforwardpodcast

1
물론 유일한 차이점은 ProcessPoolExecutor초기화 기능을 지원하지 않는다는 것입니다. 유닉스에서는 fork풀을 생성하기 전에 메인 프로세스에서 sighandler를 비활성화하고 나중에 다시 활성화함으로써 전략을 활용할 수 있습니다. 조약돌 에서는 SIGINT기본적으로 자식 프로세스를 침묵 시킵니다. 나는 그들이 파이썬 풀과 똑같이하지 않는 이유를 모른다. 결국, 사용자는 SIGINT자신을 다치게하려는 경우 처리기를 다시 설정할 수 있습니다.
noxdafox

이 솔루션은 Ctrl-C가 기본 프로세스를 방해하지 않도록하는 것으로 보입니다.
Paul 가격

1
방금 Python 3.5에서 테스트했으며 작동합니다. 어떤 버전의 Python을 사용하고 있습니까? 어떤 OS?
noxdafox

5

다중 처리 성가신 동안 예외를 만드는 두 가지 문제가있는 것 같습니다. 첫 번째 (Glenn에 의해 표시됨)는 즉각적인 응답을 얻기 위해 (즉, 전체 목록 처리를 완료하지 않고) map_async타임 아웃과 함께 사용해야한다는 것 map입니다. 두 번째 (Andrey로 표시)는 멀티 프로세싱에서 상속되지 않는 예외 Exception(예 :)를 포착하지 못한다는 것입니다 SystemExit. 다음은이 두 가지를 모두 다루는 솔루션입니다.

import sys
import functools
import traceback
import multiprocessing

def _poolFunctionWrapper(function, arg):
    """Run function under the pool

    Wrapper around function to catch exceptions that don't inherit from
    Exception (which aren't caught by multiprocessing, so that you end
    up hitting the timeout).
    """
    try:
        return function(arg)
    except:
        cls, exc, tb = sys.exc_info()
        if issubclass(cls, Exception):
            raise # No worries
        # Need to wrap the exception with something multiprocessing will recognise
        import traceback
        print "Unhandled exception %s (%s):\n%s" % (cls.__name__, exc, traceback.format_exc())
        raise Exception("Unhandled exception: %s (%s)" % (cls.__name__, exc))

def _runPool(pool, timeout, function, iterable):
    """Run the pool

    Wrapper around pool.map_async, to handle timeout.  This is required so as to
    trigger an immediate interrupt on the KeyboardInterrupt (Ctrl-C); see
    http://stackoverflow.com/questions/1408356/keyboard-interrupts-with-pythons-multiprocessing-pool

    Further wraps the function in _poolFunctionWrapper to catch exceptions
    that don't inherit from Exception.
    """
    return pool.map_async(functools.partial(_poolFunctionWrapper, function), iterable).get(timeout)

def myMap(function, iterable, numProcesses=1, timeout=9999):
    """Run the function on the iterable, optionally with multiprocessing"""
    if numProcesses > 1:
        pool = multiprocessing.Pool(processes=numProcesses, maxtasksperchild=1)
        mapFunc = functools.partial(_runPool, pool, timeout)
    else:
        pool = None
        mapFunc = map
    results = mapFunc(function, iterable)
    if pool is not None:
        pool.close()
        pool.join()
    return results

1
나는 성능 저하를 눈치 채지 못했지만 제 경우에는 function상당히 오래 지속됩니다 (수백 초).
Paul 가격

적어도 내 눈과 경험으로는 더 이상 사실이 아닙니다. 개별 하위 프로세스에서 키보드 예외를 발견하고 기본 프로세스에서 다시 키보드 예외를 포착하면 계속 사용할 수 map있으며 모든 것이 좋습니다. @Linux Cli Aik이 동작을 생성하는 솔루션을 아래에 제공했습니다. map_async기본 스레드가 하위 프로세스의 결과에 의존하는 경우 항상 사용하는 것은 아닙니다.
Code Doggo

4

당분간 최선의 해결책은 multiprocessing.pool 기능을 사용하지 않고 자신의 풀 기능을 롤링하는 것입니다. apply_async로 오류를 보여주는 예제와 풀 기능을 모두 사용하지 않는 방법을 보여주는 예제를 제공했습니다.

http://www.bryceboe.com/2010/08/26/python-multiprocessing-and-keyboardinterrupt/


매력처럼 작동합니다. 그것은 깨끗한 솔루션이며 어떤 종류의 핵 (/ me thinks)이 아닙니다.
Walter

999999 대신 9999를 사용했지만 시간 초과를 사용하면 성능 저하가 발생하지 않습니다. 예외 클래스에서 상속되지 않은 예외가 발생하면 예외가 발생합니다. 히트. 이에 대한 해결책은 모든 예외를 포착하는 것입니다 (내 솔루션 참조).
Paul Price

1

저는 파이썬 초보자입니다. 나는 모든 곳에서 답을 찾고 있었고 이것과 다른 블로그와 유튜브 비디오를 우연히 발견했다. 위의 저자 코드를 복사하여 Windows 7 64 비트의 Python 2.7.13에서 재현하려고했습니다. 내가 달성하고 싶은 것에 가깝습니다.

자식 프로세스가 ControlC를 무시하고 부모 프로세스를 종료하도록 만들었습니다. 자식 프로세스를 무시하면이 문제를 피할 수 있습니다.

#!/usr/bin/python

from multiprocessing import Pool
from time import sleep
from sys import exit


def slowly_square(i):
    try:
        print "<slowly_square> Sleeping and later running a square calculation..."
        sleep(1)
        return i * i
    except KeyboardInterrupt:
        print "<child processor> Don't care if you say CtrlC"
        pass


def go():
    pool = Pool(8)

    try:
        results = pool.map(slowly_square, range(40))
    except KeyboardInterrupt:
        pool.terminate()
        pool.close()
        print "You cancelled the program!"
        exit(1)
    print "Finally, here are the results", results


if __name__ == '__main__':
    go()

에서 시작하는 부분 pool.terminate()은 실행되지 않는 것 같습니다.


방금 이것도 알아 냈습니다! 솔직히 이것이 이것이 이와 같은 문제에 대한 최선의 해결책이라고 생각합니다. 수용 된 솔루션은 map_async사용자 에게 강요 하지만 특히 마음에 들지 않습니다. 광산과 같은 많은 상황에서 주 스레드는 개별 프로세스가 완료 될 때까지 기다려야합니다. 이것이 map존재 하는 이유 중 하나입니다 !
Code Doggo

1

다음과 같이 Pool 객체의 apply_async 메소드를 사용해보십시오.

import multiprocessing
import time
from datetime import datetime


def test_func(x):
    time.sleep(2)
    return x**2


def apply_multiprocessing(input_list, input_function):
    pool_size = 5
    pool = multiprocessing.Pool(processes=pool_size, maxtasksperchild=10)

    try:
        jobs = {}
        for value in input_list:
            jobs[value] = pool.apply_async(input_function, [value])

        results = {}
        for value, result in jobs.items():
            try:
                results[value] = result.get()
            except KeyboardInterrupt:
                print "Interrupted by user"
                pool.terminate()
                break
            except Exception as e:
                results[value] = e
        return results
    except Exception:
        raise
    finally:
        pool.close()
        pool.join()


if __name__ == "__main__":
    iterations = range(100)
    t0 = datetime.now()
    results1 = apply_multiprocessing(iterations, test_func)
    t1 = datetime.now()
    print results1
    print "Multi: {}".format(t1 - t0)

    t2 = datetime.now()
    results2 = {i: test_func(i) for i in iterations}
    t3 = datetime.now()
    print results2
    print "Non-multi: {}".format(t3 - t2)

산출:

100
Multiprocessing run time: 0:00:41.131000
100
Non-multiprocessing run time: 0:03:20.688000

이 방법의 장점은 중단 전에 처리 된 결과가 결과 사전에 리턴된다는 것입니다.

>>> apply_multiprocessing(range(100), test_func)
Interrupted by user
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

영광
스럽고

-5

이상하게도 KeyboardInterrupt아이들 을 처리 해야하는 것처럼 보입니다 . 나는 이것이 서면으로 작동 할 것으로 기대했을 것입니다 ...로 변경 slowly_square하십시오 :

def slowly_square(i):
    try:
        sleep(1)
        return i * i
    except KeyboardInterrupt:
        print 'You EVIL bastard!'
        return 0

예상대로 작동합니다.


1
나는 이것을 시도했지만 실제로 전체 작업 세트를 종료하지는 않습니다. 현재 실행중인 작업을 종료하지만 스크립트는 여전히 모든 것이 정상인 것처럼 pool.map 호출의 나머지 작업을 할당합니다.
Fragsworth

이것은 정상이지만 yuo는 발생하는 오류를 잃을 수 있습니다. 스택 추적으로 오류를 반환하면 상위 프로세스에서 오류가 발생했음을 알 수 있지만 오류가 발생해도 즉시 종료되지는 않습니다.
mehtunguh
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.