แป้นพิมพ์ขัดจังหวะด้วย Multrocessing Pool ของ python


138

ฉันจะจัดการกับเหตุการณ์ 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/…
Tiago Albineli Motta

คำตอบ:


138

นี่คือข้อบกพร่องของ Python เมื่อรอเงื่อนไขใน threading.Condition.wait () จะไม่ส่ง KeyboardInterrupt Repro:

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

ข้อยกเว้น KeyboardInterrupt จะไม่ถูกส่งจนกว่า wait () จะส่งคืนและจะไม่ส่งคืนดังนั้นการขัดจังหวะจะไม่เกิดขึ้น KeyboardInterrupt เกือบจะขัดจังหวะการรอเงื่อนไข

โปรดทราบว่าสิ่งนี้จะไม่เกิดขึ้นหากระบุการหมดเวลา cond. waiting (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

6
สิ่งนี้ไม่สามารถแก้ไขได้ บางครั้งฉันได้รับพฤติกรรมที่คาดหวังเมื่อฉันกด Control + C แต่บางครั้งก็ไม่ได้ ฉันไม่แน่ใจว่าทำไม แต่ดูเหมือนว่าอาจได้รับ KeyboardInterrupt โดยหนึ่งในกระบวนการแบบสุ่มและฉันจะได้รับพฤติกรรมที่ถูกต้องก็ต่อเมื่อกระบวนการหลักเป็นกระบวนการที่จับได้
Ryan

8
สิ่งนี้ใช้ไม่ได้กับฉันกับ Python 3.6.1 บน Windows ฉันได้รับสแต็กเทรซและขยะอื่น ๆ มากมายเมื่อฉันทำ Ctrl-C เช่นเดียวกับที่ไม่มีวิธีแก้ปัญหาดังกล่าว ในความเป็นจริงไม่มีวิธีแก้ปัญหาใดที่ฉันได้ลองจากหัวข้อนี้ดูเหมือนจะใช้งานได้ ...
szx

2
Jehejj ยังไม่แก้ไขในปี 2019 เช่นเดียวกับการทำ IO ใน paralel เป็นแนวคิดใหม่: /
Akos Lukacs

59

จากสิ่งที่ฉันพบเมื่อเร็ว ๆ นี้ทางออกที่ดีที่สุดคือตั้งค่ากระบวนการของผู้ปฏิบัติงานเพื่อละเว้น 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 () เป็นส่วนหนึ่งของลูปที่จะตรวจสอบสถานะของแต่ละโปรเซสลูกจากนั้นรีสตาร์ทกระบวนการบางอย่างด้วยความล่าช้าหากจำเป็น แทนที่จะเข้าร่วม () ที่จะรอให้กระบวนการทั้งหมดเสร็จสมบูรณ์จะตรวจสอบทีละรายการเพื่อให้แน่ใจว่ากระบวนการหลักยังคงตอบสนอง
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
ดูเหมือนว่านี่จะไม่ใช่วิธีแก้ปัญหาที่สมบูรณ์ หากKeyboardInterruptมาถึงในขณะmultiprocessingที่ทำการแลกเปลี่ยนข้อมูล IPC ของตัวเองtry..catchจะไม่มีการเปิดใช้งาน (เห็นได้ชัด)
Andrey Vlasovskikh

คุณสามารถแทนที่raise KeyboardInterruptErrorด้วยไฟล์return. คุณต้องแน่ใจว่ากระบวนการย่อยสิ้นสุดลงทันทีที่ได้รับ KeyboardInterrupt ดูเหมือนว่าค่าส่งคืนจะถูกละเว้นในmainKeyboardInterrupt ยังคงได้รับ
Bernhard

10

คำตอบที่ได้รับการโหวตไม่ได้จัดการกับปัญหาหลัก แต่เป็นผลข้างเคียงที่คล้ายกัน

เจส Noller, ผู้เขียนของห้องสมุด multiprocessing อธิบายถึงวิธีการอย่างถูกต้องจัดการกับ 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ไม่รองรับฟังก์ชัน initializer ใน Unix คุณสามารถใช้ประโยชน์จากforkกลยุทธ์ได้โดยปิดการใช้งาน sighandler ในกระบวนการหลักก่อนที่จะสร้าง Pool และเปิดใช้งานอีกครั้งในภายหลัง ในก้อนกรวดฉันปิดปากSIGINTกระบวนการย่อยโดยค่าเริ่มต้น ฉันไม่ทราบเหตุผลที่พวกเขาไม่ทำเช่นเดียวกันกับ Python Pools ในตอนท้ายผู้ใช้สามารถตั้งค่าSIGINTตัวจัดการใหม่ได้ในกรณีที่ต้องการทำร้ายตัวเอง
noxdafox

โซลูชันนี้ดูเหมือนจะป้องกันไม่ให้ Ctrl-C ขัดขวางกระบวนการหลักด้วย
ราคา Paul

1
ฉันเพิ่งทดสอบกับ Python 3.5 และใช้งานได้คุณใช้ Python เวอร์ชันใดอยู่ OS อะไร?
noxdafox

9

โดยปกติโครงสร้างง่ายๆนี้ใช้ได้กับCtrl- Cบนพูล:

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

signal.signal(signal.SIGINT, signal_handle)

ตามที่ระบุไว้ในโพสต์ที่คล้ายกัน:

จับแป้นพิมพ์ขัดจังหวะใน Python โดยไม่ต้องพยายามยกเว้น


1
สิ่งนี้จะต้องทำในแต่ละกระบวนการของผู้ปฏิบัติงานเช่นกันและอาจยังคงล้มเหลวหาก KeyboardInterrupt ถูกยกขึ้นในขณะที่ไลบรารีมัลติโพรเซสเซอร์กำลังเริ่มต้น
MarioVilas

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

ฉันพบว่าในขณะนี้วิธีแก้ปัญหาที่ดีที่สุดคืออย่าใช้คุณสมบัติมัลติโพรเซสซิง. พูล แต่จะเปิดฟังก์ชันพูลของคุณเอง ฉันให้ตัวอย่างที่สาธิตข้อผิดพลาดด้วย apply_async รวมทั้งตัวอย่างที่แสดงวิธีหลีกเลี่ยงการใช้ฟังก์ชันพูลทั้งหมด

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


ใช้งานได้เหมือนมีเสน่ห์ เป็นวิธีแก้ปัญหาที่สะอาดและไม่ใช่การแฮ็ก (/ ฉันคิดว่า) btw เคล็ดลับกับ. get (99999) ตามที่คนอื่นเสนอทำให้ประสิทธิภาพแย่
Walter

ฉันไม่สังเกตเห็นการลงโทษประสิทธิภาพใด ๆ จากการใช้การหมดเวลาแม้ว่าฉันจะใช้ 9999 แทน 999999 ข้อยกเว้นคือเมื่อมีการเพิ่มข้อยกเว้นที่ไม่ได้รับมรดกจากคลาส Exception: จากนั้นคุณต้องรอจนกว่าจะหมดเวลา ตี. วิธีแก้ปัญหาคือจับข้อยกเว้นทั้งหมด (ดูวิธีแก้ปัญหาของฉัน)
ราคา Paul

2

คุณสามารถลองใช้เมธอด 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

ตัวอย่างที่ยอดเยี่ยม
michaelvdnest

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มีอยู่จริง!
Code Doggo

-5

น่าแปลกที่ดูเหมือนว่าคุณต้องจัดการKeyboardInterruptกับเด็ก ๆ ด้วยเช่นกัน ฉันคาดหวังว่าสิ่งนี้จะทำงานได้ตามที่เขียน ... ลองเปลี่ยนslowly_squareเป็น:

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

สิ่งนี้ควรได้ผลตามที่คุณคาดไว้


1
ฉันลองแล้วและมันก็ไม่ได้ยุติงานทั้งชุด มันยุติงานที่กำลังทำงานอยู่ แต่สคริปต์ยังคงกำหนดงานที่เหลือในพูลการเรียกใช้แผนที่ราวกับว่าทุกอย่างเป็นปกติ
Fragsworth

ไม่เป็นไร แต่คุณอาจสูญเสียการติดตามข้อผิดพลาดที่เกิดขึ้น การส่งคืนข้อผิดพลาดด้วย stacktrace อาจใช้งานได้ดังนั้นกระบวนการหลักสามารถบอกได้ว่ามีข้อผิดพลาดเกิดขึ้น แต่ก็ยังไม่ออกทันทีเมื่อเกิดข้อผิดพลาด
mehtunguh
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.