Pythonのマルチプロセッシングプールでのキーボード割り込み


136

Pythonのマルチプロセッシングプールで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/...
ティアゴ・モッタAlbineli

回答:


137

これはPythonのバグです。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
このバグは公式のPythonトラッカーのどこにありますか?うまく検索できませんが、最適な検索キーワードを使用していない可能性があります。
ジョセフガービン

18
このバグは[Issue 8296] [1]として報告されています。[1]:bugs.python.org/issue8296
Andrey Vlasovskikh 2010

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()ではなく、プロセスを個別にチェックして、マスタープロセスが応答性を維持するようにします。
ジョンリース

2
したがって、参加ではなく別のメソッドを介してプロセスの完了をポーリングするのは、ビジーな待機(チェック間の小さなスリープの可能性があります)でしたか?その場合は、参加を試みる前にすべてのワーカーが完了したことを保証できるため、ブログの投稿にこのコードを含めるほうがよいでしょう。
bboe

4
これは機能しません。子供だけに信号が送られます。親はそれを受け取ることはないため、pool.terminate()実行されることはありません。子供たちに信号を無視しても何も起こりません。@Glennの答えは問題を解決します。
セリン

1
これの私のバージョンはgist.github.com/admackin/003dd646e5fadee8b8d6にあります。.join()割り込み時以外は呼び出されません- .apply_async()使用の結果を手動でチェックして、AsyncResult.ready()準備ができているかどうかを確認します。つまり、問題なく終了しました。
Andy MacKinlay 14

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しているときにが到着した場合、はアクティブ化されません(明らかに)。multiprocessingtry..catch
Andrey Vlasovskikh 2010

あなたは置き換えることができraise KeyboardInterruptErrorreturn。KeyboardInterruptを受信したらすぐに子プロセスが終了することを確認する必要があります。mainそれでもKeyboardInterruptが受信されるにもかかわらず、戻り値は無視されているようです。
Bernhard

8

通常、このシンプルな構造は、のために働くCtrl- Cプールに:

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

signal.signal(signal.SIGINT, signal_handle)

いくつかの同様の投稿で述べられたように:

try-exceptなしでPythonでキーボード割り込みをキャプチャする


1
これは各ワーカープロセスでも実行する必要があり、マルチプロセッシングライブラリの初期化中にKeyboardInterruptが発生した場合でも失敗する可能性があります。
MarioVilas 2013年

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 '12

1
もちろん、唯一の違いは、ProcessPoolExecutor初期化関数をサポートしていないことです。Unixではfork、プールを作成する前にメインプロセスでsighandlerを無効にし、後で再び有効にすることで、戦略を活用できます。で小石、私は沈黙SIGINT、デフォルトでは子プロセスに。彼らがPythonプールで同じことをしない理由を私は知りません。最後に、ユーザーはSIGINT怪我をしたい場合に備えてハンドラーを再設定できます。
noxdafox 2017

このソリューションは、Ctrl-Cがメインプロセスに割り込むことも防止するようです。
ポールプライス

1
Python 3.5でテストしたところ、動作しました。使用しているPythonのバージョンは何ですか?どんなOS?
noxdafox 2018

5

マルチプロセッシングが煩わしいときに例外を作る2つの問題があるようです。1つ目(Glennが指摘)は、即時応答を取得する(つまり、リスト全体の処理を終了しない)ためmap_asyncにでmapはなく、タイムアウトを使用する必要があることです。2つ目(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はかなり長持ちします(数百秒)。
ポールプライス14

少なくとも私の目と経験から、これは実際にはもう当てはまりません。個々の子プロセスでキーボードの例外をキャッチし、メインプロセスでもう一度それをキャッチすると、使用mapを継続でき、すべて良好です。@Linux Cli Aikこの動作を生成する以下のソリューションを提供しました。map_asyncメインスレッドが子プロセスの結果に依存している場合、使用は必ずしも望ましいとは限りません。
コードドッグゴー

4

とりあえず、最善の解決策はmultiprocessing.pool機能を使用せず、独自のプール機能をロールすることです。apply_asyncでエラーを示す例と、プール機能の使用を完全に回避する方法を示す例を提供しました。

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


魅力のように機能します。これはクリーンなソリューションであり、何らかのハック(/ me think).btwではありません。他の人が提案した.get(99999)のトリックは、パフォーマンスに悪影響を与えます。
Walter

999999の代わりに9999を使用していますが、タイムアウトの使用によるパフォーマンスの低下に気づきませんでした。例外は、Exceptionクラスから継承しない例外が発生した場合です。タイムアウトになるまで待機する必要があります。ヒット。これに対する解決策は、すべての例外をキャッチすることです(私の解決策を参照)。
ポールプライス

1

私はPythonの初心者です。私はどこでも答えを探していて、これと他のいくつかのブログやyoutubeビデオを見つけました。上記の作者のコードをコピーして貼り付け、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存在する理由の1つです。
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、2009

これは問題ありませんが、yuoは発生したエラーを追跡できなくなる可能性があります。スタックトレースでエラーを返すと、親プロセスがエラーが発生したことを認識できるように機能する場合がありますが、エラーが発生してもすぐには終了しません。
mehtunguh 2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.