键盘中断与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莫塔

Answers:


137

这是一个Python错误。等待threading.Condition.wait()中的条件时,从不发送KeyboardInterrupt。复制:

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

直到wait()返回,才会传递KeyboardInterrupt异常,并且它永远不会返回,因此中断永远不会发生。KeyboardInterrupt几乎应该可以中断条件等待。

请注意,如果指定了超时,则不会发生这种情况。cond.wait(1)将立即收到中断。因此,一种解决方法是指定超时。为此,请更换

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

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

或类似。


3
官方python跟踪器中的此错误在哪里吗?我找不到它,但是我可能没有使用最好的搜索词。
Joseph Garvin

18
该错误已归档为[问题8296] [1]。[1]:bugs.python.org/issue8296
Andrey Vlasovskikh 2010年

1
这是一种以相同方式修复pool.imap()的黑客,使得在迭代imap时可以使用Ctrl-C。捕获异常并调用pool.terminate(),程序将退出。gist.github.com/626518
亚历山大·

6
这还不能完全解决问题。有时,当我按Control + C时会得到预期的行为,而其他时候却没有。我不确定为什么,但是看起来其中的一个进程可能随机接收到KeyboardInterrupt,并且只有在父进程抓住了它的情况下,我才得到正确的行为。
瑞安·汤普森

6
对于Windows上的Python 3.6.1,这对我不起作用。当我执行Ctrl-C时,会得到大量的堆栈跟踪和其他垃圾,即与没有这种解决方法的情况相同。实际上,我从该线程尝试过的所有解决方案似乎都
无效

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 2012年

在我在生产环境中使用此代码的情况下,time.sleep()是循环的一部分,该循环将检查每个子进程的状态,然后在必要时延迟重新启动某些进程。而不是join()等待所有进程完成,而是单独检查它们,以确保主进程保持响应状态。
约翰·里斯

2
因此,是通过另一种方法(而不是联接)轮询进程完成的繁忙等待(也许在两次检查之间有少量睡眠)?如果是这种情况,那么最好将此代码包含在您的博客文章中,因为这样您就可以保证所有工作人员在尝试加入之前都已完成。
bboe 2012年

4
这行不通。仅向孩子发送信号。父级永远不会收到它,因此pool.terminate()永远不会执行。让孩子们忽略信号,什么都做不了。@Glenn的答案解决了这个问题。
塞林2014年

1
我的版本在gist.github.com/admackin/003dd646e5fadee8b8d6 ; 它不调用.join()除了在中断-它只是手动检查的结果.apply_async()运用AsyncResult.ready(),看它是否准备好了,这意味着我们已经光洁。
安迪·麦金莱

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
看来这不是一个完整的解决方案。如果KeyboardInterruptmultiprocessing执行自己的IPC数据交换时到达,try..catch则将不会激活(显然)。
安德烈·弗拉索夫斯基

你可以替换raise KeyboardInterruptError使用return。您只需要确保子进程在收到KeyboardInterrupt后立即结束。返回值似乎被忽略,main仍然收到KeyboardInterrupt。
伯恩哈德

8

通常这种简单的结构工程Ctrl- C上池:

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

signal.signal(signal.SIGINT, signal_handle)

如几篇类似文章所述:

无需尝试即可在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

1
当然,唯一的区别是ProcessPoolExecutor不支持初始化函数。在Unix上,您可以fork通过在主进程上禁用sighandler 来利用该策略,然后再创建Pool,然后再重新启用它。在pebble中SIGINT默认情况下,我对子进程保持沉默。我不知道他们对Python Pools不做的原因。最后,SIGINT如果他/她想伤害自己,用户可以重新设置处理器。
noxdafox

该解决方案似乎也可以防止Ctrl-C中断主进程。
Paul Price

1
我刚刚在Python 3.5上进行了测试,并且可以正常工作,您使用的是哪个版本的Python?什么操作系统?
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 Price

至少从我的眼光和经验来看,实际上已经不是这种情况了。如果您在单个子进程中捕获键盘异常,然后在主进程中再次捕获它,那么您可以继续使用map,一切都很好。@Linux Cli Aik在下面提供了产生此行为的解决方案。map_async如果主线程取决于子进程的结果,则并不总是需要使用。
代码Doggo

4

我发现目前最好的解决方案是不使用multiprocessing.pool功能,而是使用自己的池功能。我提供了一个使用apply_async演示该错误的示例,以及一个示例,展示了如何避免完全使用池功能。

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


奇迹般有效。这是一个干净的解决方案,而不是某种骇客(/我认为).btw,其他人提出的.get(99999)窍门严重损害了性能。
2013年

尽管我一直使用9999而不是999999,但我没有注意到使用超时会对性能造成任何影响。例外是,当引发了一个不继承自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存在的原因之一!
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}

光荣而完整的示例
eMTy

-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

可以,但是您可能会忘记所发生的错误。使用stacktrace返回错误可能会起作用,因此父进程可以告诉您发生了错误,但是在发生错误时它仍然不会立即退出。
mehtunguh 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.