Tastiera si interrompe con il pool multiprocessore di Python


136

Come posso gestire gli eventi KeyboardInterrupt con i pool multiprocessore di Python? Qui c'è un semplice esempio:

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()

Quando eseguo il codice sopra, KeyboardInterruptviene sollevato quando premo ^C, ma il processo semplicemente si blocca a quel punto e devo ucciderlo esternamente.

Voglio essere in grado di premere ^Cin qualsiasi momento e far terminare tutti i processi con grazia.


Ho risolto il mio problema utilizzando psutil, si può vedere la soluzione qui: stackoverflow.com/questions/32160054/...
Tiago Motta Albineli

Risposte:


137

Questo è un bug di Python. Durante l'attesa di una condizione in threading.Condition.wait (), KeyboardInterrupt non viene mai inviato. Repro:

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

L'eccezione KeyboardInterrupt non verrà consegnata fino a quando wait () non ritorna e non ritorna mai, quindi l'interrupt non si verifica mai. KeyboardInterrupt dovrebbe quasi sicuramente interrompere l'attesa di una condizione.

Si noti che ciò non accade se viene specificato un timeout; cond.wait (1) riceverà immediatamente l'interrupt. Pertanto, una soluzione alternativa consiste nello specificare un timeout. Per fare ciò, sostituire

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

con

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

o simili.


3
Questo bug è presente nel tracker python ufficiale da qualche parte? Ho problemi a trovarlo, ma probabilmente non sto usando i migliori termini di ricerca.
Joseph Garvin,

18
Questo errore è stato archiviato come [Numero 8296] [1]. [1]: bugs.python.org/issue8296
Andrey Vlasovskikh

1
Ecco un trucco che corregge pool.imap () allo stesso modo, rendendo possibile Ctrl-C quando si scorre su imap. Cattura l'eccezione e chiama pool.terminate () e il tuo programma verrà chiuso. gist.github.com/626518
Alexander Ljungberg

6
Questo non risolve del tutto le cose. A volte ottengo il comportamento previsto quando premo Control + C, altre volte no. Non sono sicuro del perché, ma sembra che KeyboardInterrupt sia ricevuto casualmente da uno dei processi e ottengo il comportamento corretto solo se il processo genitore è quello che lo cattura.
Ryan C. Thompson,

6
Questo non funziona per me con Python 3.6.1 su Windows. Ricevo tonnellate di tracce di stack e altra immondizia quando faccio Ctrl-C, vale a dire lo stesso senza una tale soluzione alternativa. In effetti nessuna delle soluzioni che ho provato da questo thread sembra funzionare ...
szx

56

Da quello che ho scoperto di recente, la soluzione migliore è impostare i processi di lavoro in modo da ignorare del tutto SIGINT e limitare tutto il codice di pulizia al processo padre. Ciò risolve il problema per i processi di lavoro inattivi e occupati e non richiede errori nella gestione del codice nei processi figlio.

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()

Spiegazione e codice di esempio completo sono disponibili su http://noswap.com/blog/python-multiprocessing-keyboardinterrupt/ e http://github.com/jreese/multiprocessing-keyboardinterrupt rispettivamente.


4
Ciao John. La tua soluzione non realizza la stessa cosa della mia soluzione sì, purtroppo complicata. Si nasconde dietro il time.sleep(10)nel processo principale. Se dovessi rimuovere quella sospensione o se aspetti che il processo tenti di unirsi al pool, cosa che devi fare per garantire che i lavori siano completi, allora soffri ancora dello stesso problema che il processo principale non ha non riceve KeyboardInterrupt mentre è in attesa di un'operazione di polling join.
Bboe

Nel caso in cui ho usato questo codice in produzione, time.sleep () faceva parte di un ciclo che controllava lo stato di ciascun processo figlio e quindi riavviava alcuni processi in caso di ritardo, se necessario. Invece di join () che aspetterebbe il completamento di tutti i processi, li controllerebbe individualmente, assicurando che il processo principale rimanesse reattivo.
John Reese,

2
Quindi è stata più un'attesa impegnativa (forse con piccoli dormienti tra i controlli) che ha richiesto il completamento del processo tramite un altro metodo anziché unirsi? In tal caso, forse sarebbe meglio includere questo codice nel tuo post sul blog, dal momento che puoi quindi garantire che tutti i lavoratori abbiano completato prima di provare ad aderire.
Bboe

4
Questo non funziona Solo i bambini ricevono il segnale. Il genitore non lo riceve mai, quindi pool.terminate()non viene mai eseguito. Far ignorare ai bambini il segnale non ottiene nulla. @ La risposta di Glenn risolve il problema.
Cerin,

1
La mia versione di questo è su gist.github.com/admackin/003dd646e5fadee8b8d6 ; non chiama .join()se non in caso di interruzione: controlla semplicemente manualmente il risultato .apply_async()dell'uso AsyncResult.ready()per vedere se è pronto, il che significa che abbiamo terminato in modo pulito.
Andy MacKinlay,

29

Per alcuni motivi, Exceptionvengono gestite normalmente solo le eccezioni ereditate dalla classe base . Per ovviare al problema, puoi aumentare nuovamente il tuo KeyboardInterruptcome Exceptionistanza:

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()

Normalmente otterresti il ​​seguente output:

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

Quindi se colpisci ^C, otterrai:

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

2
Sembra che questa non sia una soluzione completa. Se un KeyboardInterruptè arrivato mentre multiprocessingsta eseguendo il proprio scambio di dati IPC, allora try..catchnon sarà attivato (ovviamente).
Andrey Vlasovskikh,

È possibile sostituire raise KeyboardInterruptErrorcon a return. Devi solo assicurarti che il processo figlio finisca non appena viene ricevuto KeyboardInterrupt. Il valore restituito sembra essere ignorato, in ogni caso mainviene ricevuto KeyboardInterrupt.
Bernhard

8

Di solito questa semplice struttura funziona per Ctrl- Csu Pool:

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

signal.signal(signal.SIGINT, signal_handle)

Come è stato affermato in alcuni post simili:

Cattura l'interruzione della tastiera in Python senza provare-tranne


1
Questo dovrebbe essere fatto anche su ciascuno dei processi di lavoro, e potrebbe comunque fallire se KeyboardInterrupt viene generato durante l'inizializzazione della libreria multiprocessore.
MarioVilas,

7

La risposta votata non affronta la questione centrale ma un effetto collaterale analogo.

Jesse Noller, l'autore della libreria multiprocessing, spiega come gestire correttamente CTRL + C quando lo si utilizza multiprocessing.Poolin un vecchio post sul blog .

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()

Ho scoperto che anche ProcessPoolExecutor ha lo stesso problema. L'unica soluzione che sono riuscito a trovare è stata quella di chiamare os.setpgrp()dall'interno del futuro
portforwardpodcast,

1
Certo, l'unica differenza è che ProcessPoolExecutornon supporta le funzioni di inizializzazione. Su Unix, è possibile sfruttare la forkstrategia disabilitando il sighandler sul processo principale prima di creare il pool e riattivarlo in seguito. In ghiaia , tace SIGINTsui processi figlio per impostazione predefinita. Non sono a conoscenza del motivo per cui non fanno lo stesso con i pool Python. Alla fine, l'utente potrebbe reimpostare il SIGINTgestore nel caso in cui voglia ferirsi.
noxdafox,

Questa soluzione sembra impedire anche a Ctrl-C di interrompere il processo principale.
Paul Price,

1
Ho appena testato su Python 3.5 e funziona, quale versione di Python stai usando? Quale sistema operativo?
noxdafox,

5

Sembra che ci siano due problemi che rendono le eccezioni mentre il multiprocessing è fastidioso. Il primo (notato da Glenn) è che è necessario utilizzare map_asynccon un timeout anziché mapper ottenere una risposta immediata (ovvero, non terminare l'elaborazione dell'intero elenco). Il secondo (notato da Andrey) è che il multiprocessing non rileva eccezioni che non ereditano Exception(ad es SystemExit.). Quindi ecco la mia soluzione che si occupa di entrambi:

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
Non ho notato alcuna penalità per le prestazioni, ma nel mio caso functionè abbastanza longevo (centinaia di secondi).
Paul Price,

Questo in realtà non è più il caso, almeno dai miei occhi ed esperienza. Se si rileva l'eccezione della tastiera nei singoli processi figlio e la si ripresenta nel processo principale, è possibile continuare a utilizzare mape tutto è a posto. @Linux Cli Aikfornito una soluzione di seguito che produce questo comportamento. L'uso map_asyncnon è sempre desiderato se il thread principale dipende dai risultati dai processi figlio.
Codice Doggo,

4

Ho scoperto, per il momento, la soluzione migliore è quella di non utilizzare la funzione multiprocessing.pool ma piuttosto di ruotare la propria funzionalità pool. Ho fornito un esempio che dimostra l'errore con apply_async e un esempio che mostra come evitare di utilizzare completamente la funzionalità del pool.

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


Funziona come un fascino. È una soluzione pulita e non una sorta di hack (/ me pensa) .btw, il trucco con .get (99999) come proposto da altri fa male alle prestazioni.
Walter,

Non ho notato alcuna penalità di prestazione dall'uso di un timeout, anche se ho usato 9999 anziché 999999. L'eccezione è quando viene sollevata un'eccezione che non eredita dalla classe Exception: quindi devi attendere fino a quando il timeout è colpire. La soluzione a questo è catturare tutte le eccezioni (vedi la mia soluzione).
Paul Price,

1

Sono un principiante in Python. Cercavo ovunque risposte e inciampare in questo e in alcuni altri blog e video di YouTube. Ho provato a copiare incollare il codice dell'autore sopra e riprodurlo sul mio Python 2.7.13 in Windows 7 a 64 bit. È vicino a ciò che voglio ottenere.

Ho fatto i miei processi figlio per ignorare il ControlC e far terminare il processo genitore. Sembra che bypassare il processo figlio eviti questo problema per me.

#!/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()

La parte che inizia da pool.terminate()non sembra mai essere eseguita.


Ho appena capito anche questo! Sinceramente penso che questa sia la soluzione migliore per un problema come questo. La soluzione accettata impone map_asyncall'utente, cosa che non mi piace particolarmente. In molte situazioni, come la mia, il thread principale deve attendere il completamento dei singoli processi. Questo è uno dei motivi per cui mapesiste!
Codice Doggo,

1

Puoi provare a utilizzare il metodo apply_async di un oggetto Pool, in questo modo:

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)

Produzione:

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

Un vantaggio di questo metodo è che i risultati elaborati prima dell'interruzione verranno restituiti nel dizionario dei risultati:

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

Esempio glorioso e completo
eMTy

-5

Stranamente sembra che tu debba gestire anche KeyboardInterrupti bambini. Mi sarei aspettato che funzionasse come scritto ... prova a cambiare slowly_squarein:

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

Dovrebbe funzionare come previsto.


1
Ho provato questo, e in realtà non termina l'intero set di lavori. Termina i lavori in esecuzione, ma lo script assegna comunque i lavori rimanenti nella chiamata pool.map come se tutto fosse normale.
Fragsworth,

va bene, ma potresti perdere la traccia degli errori che si verificano. la restituzione dell'errore con stacktrace potrebbe funzionare in modo che il processo parent possa dire che si è verificato un errore, ma non si interrompe immediatamente quando si verifica l'errore.
mehtunguh,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.