Клавіатура Переривається з багатопроцесорним пулом python


136

Як я можу обробляти події KeyboardInterrupt за допомогою багатопроцесорних пулів python? Ось простий приклад:

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
Чи є ця помилка в офіційному трейдері пітона? У мене виникають проблеми з її пошуку, але я, мабуть, просто не використовую найкращих пошукових термінів.
Джозеф Гарвін

18
Ця помилка була подана як [Issue 8296] [1]. [1]: bugs.python.org/issue8296
Андрій Власовських

1
Ось хак, який виправляє pool.imap () таким же чином, що робить Ctrl-C можливим під час ітерації над зображенням. Ловіть виняток і виклику pool.terminate (), і ваша програма вийде. gist.github.com/626518
Олександр

6
Це не зовсім виправляє речі. Іноді я отримую очікувану поведінку, коли натискаю Control + C, а інший раз - ні. Я не впевнений, чому, але, схоже, можливо, KeyboardInterrupt отримується одним із процесів навмання, і я отримую правильну поведінку лише в тому випадку, якщо батьківський процес є таким, який його сприймає.
Райан К. Томпсон,

6
Це не працює для мене з Python 3.6.1 в Windows. Коли я роблю 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)в основному процесі. Якщо вам потрібно було зняти цей сон, або якщо ви зачекаєте, доки процес не зможе приєднатися до пулу, що ви повинні зробити для того, щоб гарантувати, що завдання виконані, ви все одно страждаєте від тієї самої проблеми, яка є основною процедурою. Не отримаю KeyboardInterrupt, поки він чекає на опитування join.
bboe

У випадку, коли я використовував цей код у виробництві, time.sleep () було частиною циклу, який би перевіряв стан кожного дочірнього процесу, а потім за необхідності перезапускав певні процеси. Замість того, щоб приєднатись до (), який очікував би завершення всіх процесів, він перевірятиме їх окремо, гарантуючи, що головний процес залишається чутливим.
Джон Різ

2
Отож, більш зайнятим чеканням (можливо, з невеликим сном між чеками) було проведено опитування для завершення процесу іншим методом, а не приєднанням? Якщо це так, можливо, було б краще включити цей код до своєї публікації блогу, оскільки ви можете гарантувати, що всі працівники завершили роботу перед спробою вступу.
bboe

4
Це не працює. Лише діти надсилають сигнал. Батько ніколи його не отримує, тому pool.terminate()ніколи не отримує страти. Якщо діти ігнорують сигнал, це нічого не досягає. @ Відповідь Глена вирішує проблему.
Серін

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
Здається, це не повне рішення. Якщо a надійде KeyboardInterruptпід час multiprocessingздійснення власного обміну даними IPC, то воно try..catchне буде активовано (очевидно).
Андрій Власовських

Ви можете замінити raise KeyboardInterruptErrorна return. Вам просто потрібно переконатися, що дочірній процес закінчується, як тільки надходить KeyboardInterrupt. Зворотне значення, здається, ігнорується, mainоскільки все-таки отримано перерва клавіатури.
Бернхард

8

Зазвичай ця проста структура працює для Ctrl- Cв Pool:

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

signal.signal(signal.SIGINT, signal_handle)

Як було сказано в кількох подібних дописах:

Захоплення клавіатури переривання в Python без спроб за винятком


1
Це повинно бути зроблено і на кожному з робочих процесів, і все ще може вийти з ладу, якщо KeyboardInterrupt буде піднято під час ініціалізації багатопроцесорної бібліотеки.
MarioVilas

7

Проголосована відповідь стосується не основного питання, а подібного побічного ефекту.

Джессі Ноллер, автор багатопроцесорної бібліотеки, пояснює, як правильно боротися з CTRL + C при використанні multiprocessing.Poolв старому дописі блогу .

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 на основному процесі перед створенням пулу та повторно ввімкнувши його після цього. У камінчику я замовчую SIGINTнад дитиною процеси за замовчуванням. Мені невідомо, з якої причини вони не роблять те саме з Python Pools. Зрештою, користувач міг би знову встановити SIGINTобробник у випадку, якщо він хоче завдати собі шкоди.
noxdafox

Це рішення, здається, не дозволяє Ctrl-C також перервати основний процес.
Пол Прайс

1
Я щойно тестував на Python 3.5, і він працює, яку версію Python ви використовуєте? Яка ОС?
noxdafox

5

Здається, є два питання, які роблять винятки, при цьому багатопроцесорні дратівливі. Перший (зазначив Гленн) полягає в тому, що вам потрібно скористатися map_asyncз таймаутом замість mapтого, щоб отримати негайну відповідь (тобто не закінчувати обробку всього списку). Друга (зазначила Андрій) полягає в тому, що багатопроцесорна робота не сприймає винятків, які не успадковуються 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це досить довговічний (сотні секунд).
Пол Ціна

Насправді це вже не так, принаймні з моїх очей та досвіду. Якщо ви виберете виняток з клавіатури в окремих дочірніх процесах і ще раз його в основному процесі, то ви можете продовжувати використовувати, mapі все добре. @Linux Cli Aikнадано рішення, яке виробляє таку поведінку. Використовувати map_asyncне завжди бажано, якщо основна нитка залежить від результатів дочірніх процесів.
Код Догго

4

Наразі я знайшов, що найкращим рішенням є не використовувати функцію multiprocessing.pool, а скоріше скрутити власну функціональність пулу. Я наводив приклад, що демонструє помилку у Apply_async, а також приклад, який показує, як взагалі не використовувати функціональність пулу.

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


Працює як шарм. Це чисте рішення, а не якийсь злом (/ мені здається) .btw, трюк із .get (99999), запропонований іншими, шкодить продуктивності.
Вальтер

Я не помітив жодного штрафу за ефективність використання тайм-ауту, хоча я використовував 9999 замість 999999. Виняток - це коли виняток, який не успадковується з класу «Виняток», підвищено: тоді вам доведеться почекати, поки час очікування буде хіт. Рішенням цього є пошук усіх винятків (див. Моє рішення).
Пол Прайс

1

Я новачок у Python. Я всюди шукав відповіді і натрапляв на це та кілька інших блогів та відео YouTube. Я спробував скопіювати вищезазначений авторський код та відтворити його на своєму python 2.7.13 у Windows 7 64-розрядному. Це близько до того, що я хочу досягти.

Я змусив своїх дочірніх процесів ігнорувати 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

Ви можете спробувати скористатися методом apply_async об'єкта Pool, наприклад таким:

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

це нормально, але yuo може втратити повідомлення про помилки, які трапляються. повернення помилки за допомогою стек-трек може працювати, щоб батьківський процес міг сказати, що сталася помилка, але вона все одно не виходить негайно, коли виникає помилка.
mehtunguh
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.