Bàn phím bị gián đoạn với nhóm đa xử lý của python


136

Làm cách nào tôi có thể xử lý các sự kiện Bàn phím bị gián đoạn với Nhóm đa xử lý của python? Đây là một ví dụ đơn giản:

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

Khi chạy mã ở trên, mã KeyboardInterruptđược tăng lên khi tôi nhấn ^C, nhưng quá trình chỉ đơn giản là bị treo ở điểm đó và tôi phải giết nó bên ngoài.

Tôi muốn có thể nhấn ^Cbất cứ lúc nào và khiến tất cả các quá trình thoát ra một cách duyên dáng.


Tôi giải quyết vấn đề của tôi sử dụng psutil, bạn sẽ nhìn thấy giải pháp ở đây: stackoverflow.com/questions/32160054/...
Tiago Albineli Motta

Câu trả lời:


137

Đây là một lỗi Python. Khi chờ một điều kiện trong luồng .ondond.wait (), KeyboardInterrupt không bao giờ được gửi. Trả lời:

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

Ngoại lệ KeyboardInterrupt sẽ không được gửi cho đến khi Wait () trả về và nó không bao giờ quay trở lại, vì vậy việc ngắt không bao giờ xảy ra. Bàn phím gần như chắc chắn sẽ làm gián đoạn một điều kiện chờ đợi.

Lưu ý rằng điều này không xảy ra nếu thời gian chờ được chỉ định; cond.wait (1) sẽ nhận được ngắt ngay lập tức. Vì vậy, một cách giải quyết là chỉ định thời gian chờ. Để làm điều đó, thay thế

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

với

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

hoặc tương tự.


3
Là lỗi này trong trình theo dõi trăn chính thức ở bất cứ đâu? Tôi gặp khó khăn khi tìm nó nhưng có lẽ tôi chỉ không sử dụng thuật ngữ tìm kiếm tốt nhất.
Joseph Garvin

18
Lỗi này đã được gửi là [Vấn đề 8296] [1]. [1]: bug.python.org/su8296
Andrey Vlasovskikh

1
Đây là một bản sửa lỗi pool.imap () theo cách tương tự, làm cho Ctrl-C có thể khi lặp qua imap. Bắt ngoại lệ và gọi pool.terminate () và chương trình của bạn sẽ thoát. gist.github.com/626518
Alexander Ljungberg

6
Điều này không hoàn toàn sửa chữa mọi thứ. Đôi khi tôi nhận được hành vi mong đợi khi tôi nhấn Control + C, lần khác thì không. Tôi không chắc tại sao, nhưng có vẻ như Bàn phím được nhận ngẫu nhiên bởi một trong các quy trình và tôi chỉ nhận được hành vi chính xác nếu quy trình cha mẹ là quy trình bắt được nó.
Ryan C. Thompson

6
Điều này không phù hợp với tôi với Python 3.6.1 trên Windows. Tôi nhận được vô số dấu vết ngăn xếp và rác khác khi tôi làm Ctrl-C, nghĩa là không có cách giải quyết như vậy. Trên thực tế, không có giải pháp nào tôi đã thử từ chủ đề này dường như hoạt động ...
szx

56

Từ những gì tôi đã tìm thấy gần đây, giải pháp tốt nhất là thiết lập các quy trình worker để bỏ qua SIGINT hoàn toàn và giới hạn tất cả các mã dọn dẹp cho quy trình cha. Điều này khắc phục sự cố cho cả quy trình công nhân nhàn rỗi và bận rộn và không yêu cầu mã xử lý lỗi trong các quy trình con của bạn.

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

Giải thích và mã ví dụ đầy đủ có thể được tìm thấy tại http://noswap.com/blog/python-multiprocessing-keyboardinterrupt/http://github.com/jreese/multiprocessing-keyboardinterrupt tương ứng.


4
Chào John. Giải pháp của bạn không thực hiện được điều tương tự như giải pháp của tôi, vâng, không may phức tạp. Nó ẩn đằng sau time.sleep(10)quá trình chính. Nếu bạn đã loại bỏ giấc ngủ đó hoặc nếu bạn đợi cho đến khi quá trình cố gắng tham gia vào nhóm mà bạn phải làm để đảm bảo công việc hoàn tất, thì bạn vẫn gặp phải vấn đề tương tự, đó là quy trình chính không xảy ra. Tôi không nhận được Bàn phím trong khi nó đang chờ joinhoạt động thăm dò ý kiến .
bboe

Trong trường hợp tôi sử dụng mã này trong sản xuất, time.s ngủ () là một phần của vòng lặp sẽ kiểm tra trạng thái của từng quy trình con và sau đó khởi động lại một số quy trình bị trì hoãn nếu cần. Thay vì tham gia () sẽ chờ tất cả các quy trình hoàn tất, nó sẽ kiểm tra riêng chúng, đảm bảo rằng quy trình chính vẫn phản hồi.
John Reese

2
Vì vậy, đó là một sự chờ đợi bận rộn hơn (có thể với những giấc ngủ nhỏ giữa các lần kiểm tra) được thăm dò để hoàn thành quá trình thông qua một phương pháp khác chứ không phải tham gia? Nếu đó là trường hợp, có lẽ tốt hơn là bao gồm mã này trong bài đăng trên blog của bạn, vì sau đó bạn có thể đảm bảo rằng tất cả các công nhân đã hoàn thành trước khi cố gắng tham gia.
bboe

4
Điều này không hoạt động. Chỉ có những đứa trẻ được gửi tín hiệu. Cha mẹ không bao giờ nhận được nó, vì vậy pool.terminate()không bao giờ được thực hiện. Có con bỏ qua tín hiệu hoàn thành không có gì. Câu trả lời của @ Glenn giải quyết vấn đề.
Cerin

1
Phiên bản này của tôi là tại gist.github.com/admackin/003dd646e5fadee8b8d6 ; nó không gọi .join()ngoại trừ ngắt - nó chỉ đơn giản kiểm tra kết quả của .apply_async()việc sử dụng AsyncResult.ready()để xem nó đã sẵn sàng chưa, có nghĩa là chúng tôi đã hoàn thành một cách sạch sẽ.
Andy MacKinlay

29

Vì một số lý do, chỉ các ngoại lệ được kế thừa từ Exceptionlớp cơ sở được xử lý bình thường. Như một giải pháp thay thế, bạn có thể nâng cao lại KeyboardInterruptnhư một Exceptionví dụ:

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

Thông thường bạn sẽ nhận được đầu ra sau:

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

Vì vậy, nếu bạn đánh ^C, bạn sẽ nhận được:

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

2
Có vẻ như đây không phải là một giải pháp hoàn chỉnh. Nếu a KeyboardInterruptđến trong khi multiprocessingđang thực hiện trao đổi dữ liệu IPC của chính nó thì nó try..catchsẽ không được kích hoạt (rõ ràng).
Andrey Vlasovskikh

Bạn có thể thay thế raise KeyboardInterruptErrorbằng một return. Bạn chỉ cần đảm bảo rằng tiến trình con kết thúc ngay sau khi nhận được KeyboardInterrupt. Giá trị trả về dường như bị bỏ qua, trong khi mainvẫn nhận được Bàn phím tắt.
Bernhard

8

Thông thường cấu trúc đơn giản này hoạt động cho Ctrl- Ctrên Pool:

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

signal.signal(signal.SIGINT, signal_handle)

Như đã nêu trong một số bài viết tương tự:

Chụp bàn phím bị gián đoạn trong Python mà không cần thử ngoại trừ


1
Điều này cũng sẽ phải được thực hiện trên mỗi quy trình worker, và vẫn có thể thất bại nếu KeyboardInterrupt được nâng lên trong khi thư viện đa xử lý đang khởi tạo.
MarioVilas

7

Câu trả lời được bình chọn không giải quyết vấn đề cốt lõi mà là một tác dụng phụ tương tự.

Jesse Noller, tác giả của thư viện đa xử lý, giải thích cách xử lý chính xác với CTRL + C khi sử dụng multiprocessing.Pooltrong một bài đăng trên blog 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()

Tôi đã thấy rằng ProcessPoolExecutor cũng có vấn đề tương tự. Cách khắc phục duy nhất tôi có thể tìm thấy là gọi os.setpgrp()từ bên trong tương lai
portforwardpodcast

1
Chắc chắn, sự khác biệt duy nhất là ProcessPoolExecutorkhông hỗ trợ các chức năng khởi tạo. Trên Unix, bạn có thể tận dụng forkchiến lược bằng cách vô hiệu hóa bộ thở trong quy trình chính trước khi tạo Pool và kích hoạt lại nó sau đó. Trong sỏi , tôi im lặng SIGINTtrên các tiến trình con theo mặc định. Tôi không biết lý do họ không làm như vậy với Python Pools. Cuối cùng, người dùng có thể thiết lập lại SIGINTtrình xử lý trong trường hợp anh ta / cô ta muốn làm tổn thương chính mình.
noxdafox

Giải pháp này dường như cũng ngăn Ctrl-C làm gián đoạn quá trình chính.
Paul Giá

1
Tôi mới thử nghiệm trên Python 3.5 và nó hoạt động, bạn đang sử dụng phiên bản Python nào? HĐH gì?
noxdafox

5

Có vẻ như có hai vấn đề khiến ngoại lệ trong khi xử lý nhiều vấn đề gây khó chịu. Điều đầu tiên (được Glenn lưu ý) là bạn cần sử dụng map_asyncvới thời gian chờ thay vì mapđể nhận được phản hồi ngay lập tức (nghĩa là không hoàn thành xử lý toàn bộ danh sách). Thứ hai (được ghi chú bởi Andrey) là đa xử lý không bắt được các ngoại lệ không được thừa hưởng từ Exception(ví dụ SystemExit:). Vì vậy, đây là giải pháp của tôi liên quan đến cả hai điều này:

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
Tôi đã không nhận thấy bất kỳ hình phạt hiệu suất, nhưng trong trường hợp của tôi functionlà khá lâu (hàng trăm giây).
Paul Giá

Đây thực sự không phải là trường hợp nữa, ít nhất là từ đôi mắt và kinh nghiệm của tôi. Nếu bạn bắt ngoại lệ bàn phím trong các tiến trình con riêng lẻ và bắt nó một lần nữa trong quy trình chính, thì bạn có thể tiếp tục sử dụng mapvà tất cả đều tốt. @Linux Cli Aikcung cấp một giải pháp dưới đây tạo ra hành vi này. Việc sử dụng map_asynckhông phải lúc nào cũng mong muốn nếu luồng chính phụ thuộc vào kết quả từ các tiến trình con.
Mã Doggo

4

Tôi thấy, hiện tại, giải pháp tốt nhất là không sử dụng tính năng multirocessing.pool mà thay vào đó là cuộn chức năng pool của riêng bạn. Tôi đã cung cấp một ví dụ minh họa lỗi với application_async cũng như một ví dụ chỉ ra cách tránh sử dụng chức năng nhóm hoàn toàn.

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


Hoạt động như một lá bùa. Đó là một giải pháp sạch và không phải là một loại hack (/ tôi nghĩ) .btw, thủ thuật với .get (99999) như đề xuất của những người khác làm tổn hại đến hiệu suất.
Walter

Tôi đã không nhận thấy bất kỳ hình phạt hiệu suất nào khi sử dụng thời gian chờ, mặc dù tôi đã sử dụng 9999 thay vì 999999. Ngoại lệ là khi một ngoại lệ không được thừa hưởng từ lớp Exception được nêu ra: thì bạn phải đợi cho đến khi hết thời gian đánh. Giải pháp cho vấn đề đó là bắt tất cả các ngoại lệ (xem giải pháp của tôi).
Paul Giá

1

Tôi là người mới chơi Python. Tôi đã tìm kiếm khắp nơi để tìm câu trả lời và vấp phải điều này và một vài blog và video youtube khác. Tôi đã cố gắng sao chép dán mã của tác giả ở trên và sao chép nó trên python 2.7.13 của tôi trong windows 7 64-bit. Nó gần với những gì tôi muốn đạt được.

Tôi đã tạo cho con tôi các quy trình để bỏ qua ControlC và làm cho quá trình cha mẹ chấm dứt. Có vẻ như bỏ qua quá trình con không tránh được vấn đề này đối với tôi.

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

Phần bắt đầu pool.terminate()dường như không bao giờ thực hiện.


Tôi chỉ cần tìm ra điều này là tốt! Tôi thành thật nghĩ rằng đây là giải pháp tốt nhất cho một vấn đề như thế này. Giải pháp được chấp nhận buộc map_asyncngười dùng, điều mà tôi không đặc biệt thích. Trong nhiều tình huống, như của tôi, luồng chính cần chờ các tiến trình riêng lẻ kết thúc. Đây là một trong những lý do tại sao maptồn tại!
Mã Doggo

1

Bạn có thể thử sử dụng phương thức application_async của đối tượng Pool, như thế này:

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)

Đầu ra:

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

Một lợi thế của phương pháp này là các kết quả được xử lý trước khi gián đoạn sẽ được trả về trong từ điển kết quả:

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

Ví dụ vinh quang và đầy đủ
eMTy

-5

Thật kỳ lạ, có vẻ như bạn cũng phải xử lý KeyboardInterruptcả trẻ em. Tôi đã mong đợi điều này sẽ hoạt động như được viết ... hãy thử đổi slowly_squarethành:

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

Điều đó sẽ làm việc như bạn mong đợi.


1
Tôi đã thử điều này và nó không thực sự chấm dứt toàn bộ các công việc. Nó chấm dứt các công việc hiện đang chạy, nhưng tập lệnh vẫn gán các công việc còn lại trong lệnh gọi pool.map như thể mọi thứ đều bình thường.
Fragsworth

Điều này không sao, nhưng yuo có thể mất dấu vết lỗi xảy ra. trả về lỗi với stacktrace có thể hoạt động để quá trình cha có thể biết rằng đã xảy ra lỗi, nhưng nó vẫn không thoát ngay lập tức khi xảy ra lỗi.
mehtunguh
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.