我有自定义信号处理的python multiprocessing
设置(即 worker 进程),这可以防止工作人员干净地使用multiprocessing
本身。 (请参阅下面的扩展问题说明)。
生成所有工作进程的 master 类如下所示(某些部分被剥离为仅包含重要部分)。
此处,它仅重新绑定自己的signal
以打印Master teardown
;实际上,接收到的信号沿着进程树传播,必须由工作人员自己处理。这是通过在产生之后重新绑定信号来实现的。
class Midlayer(object):
def __init__(self, nprocs=2):
self.nprocs = nprocs
self.procs = []
def handle_signal(self, signum, frame):
log.info('Master teardown')
for p in self.procs:
p.join()
sys.exit()
def start(self):
# Start desired number of workers
for _ in range(nprocs):
p = Worker()
self.procs.append(p)
p.start()
# Bind signals for master AFTER workers have been spawned and started
signal.signal(signal.SIGINT, self.handle_signal)
signal.signal(signal.SIGTERM, self.handle_signal)
# Serve forever, only exit on signals
for p in self.procs:
p.join()
工作人员类基于multiprocessing.Process
并实现了自己的run()
方法。
在此方法中,它连接到分布式消息队列并轮询队列中的项 forever 。 永远应该是:在工作人员收到SIGINT
或SIGTERM
之前。工人不应该立即戒烟;相反,它必须完成它所做的任何计算,然后退出(quit_req
设置为True
后)。
class Worker(Process):
def __init__(self):
self.quit_req = False
Process.__init__(self)
def handle_signal(self, signum, frame):
print('Stopping worker (pid: {})'.format(self.pid))
self.quit_req = True
def run(self):
# Set signals for worker process
signal.signal(signal.SIGINT, self.handle_signal)
signal.signal(signal.SIGTERM, self.handle_signal)
q = connect_to_some_distributed_message_queue()
# Start consuming
print('Starting worker (pid: {})'.format(self.pid))
while not self.quit_req:
message = q.poll()
if len(message):
try:
print('{} handling message "{}"'.format(
self.pid, message)
)
# Facade pattern: Pick the correct target function for the
# requested message and execute it.
MessageRouter.route(message)
except Exception as e:
print('{} failed handling "{}": {}'.format(
self.pid, message, e.message)
)
到目前为止基本设置,(几乎)一切正常:
现在出现问题:目标函数(message
外观导向MessageRouter
)可能包含非常复杂的业务逻辑,因此可能需要多处理。 / p>
例如,如果目标函数包含这样的内容:
nproc = 4
# Spawn a pool, because we have expensive calculation here
p = Pool(processes=nproc)
# Collect result proxy objects for async apply calls to 'some_expensive_calculation'
rpx = [p.apply_async(some_expensive_calculation, ()) for _ in range(nproc)]
# Collect results from all processes
res = [rpx.get(timeout=.5) for r in rpx]
# Print all results
print(res)
然后Pool
产生的进程也会将SIGINT
和SIGTERM
的信号处理重定向到工作人员的handle_signal
功能(因为信号传播)到过程子树),基本上打印Stopping worker (pid: ...)
而不是停止。我知道,这是因为我在生成其子进程之前重新绑定了worker 的信号。
这就是我被困的地方:我无法设置工人'在产生子进程之后发出信号,因为我不知道它是否会生成一些(目标函数被屏蔽并且可能被其他人写入),并且因为工作者在其轮询中保留(按设计) -环。同时,我不能指望使用multiprocessing
的目标函数的实现将其自己的信号处理程序重新绑定到(无论如何)默认值。
目前,我想在工作程序的每个循环中恢复信号处理程序(在将消息路由到其目标函数之前)并在函数返回后重置它们是唯一的选择,但它只是感觉不对。
我错过了什么吗?你有什么建议吗?如果有人能给我一个如何解决我的设计缺陷的暗示,我真的很开心!
答案 0 :(得分:2)
没有一种明确的方法可以按照您想要的方式解决问题。在多处理环境中,我经常发现自己必须运行未知代码(表示为Python入口点函数,这可能会导致某些C怪异)。
这就是我解决问题的方法。
主循环
通常主循环非常简单,它从某个源(HTTP,Pipe,Rabbit Queue ...)获取任务并将其提交给工作池。我确保正确处理KeyboardInterrupt异常以关闭服务。
try:
while 1:
task = get_next_task()
service.process(task)
except KeyboardInterrupt:
service.wait_for_pending_tasks()
logging.info("Sayonara!")
工人
工作人员由来自multiprocessing.Pool
或concurrent.futures.ProcessPoolExecutor
的工作人员管理。如果我需要更高级的功能,例如超时支持,我可以使用billiard或pebble。
每个工作人员将按照建议here忽略SIGINT。 SIGTERM保留为默认值。
服务
该服务由systemd或supervisord控制。在任何一种情况下,我都要确保终止请求始终作为SIGINT(CTL + C)传递。
我希望将SIGTERM作为紧急关闭而不是仅依靠SIGKILL。 SIGKILL不可移植,有些平台不实现它。
“我觉得这很简单”
如果事情更复杂,我会考虑使用Luigi或Celery等框架。
总的来说,重新发明这些东西是非常有害的,并且给予很少的满足感。特别是如果其他人必须查看该代码。
如果你的目的是要了解这些事情是如何完成的,那么后一句不适用。
答案 1 :(得分:2)
我能够使用Python 3和set_start_method(method)
使用'forkserver'
flavour执行此操作。 Python 3的另一种方式> Python 2!
"这个"我的意思是:
然后在Ctrl-C上执行以下操作:
stop
标志并继续执行完成他们的工作,虽然我没有在我的例子中烦恼,我刚刚加入了我认识的孩子)然后退出。当然请注意,如果您打算让工作人员的孩子不要崩溃,那么您需要在工作进程run()
方法或某个地方安装一些忽略处理程序或其他东西。
无情地解除文件:
当程序启动并选择forkserver start方法时,将启动服务器进程。从那时起,每当需要一个新进程时,父进程就会连接到服务器并请求它分叉一个新进程。 fork服务器进程是单线程的,因此使用os.fork()是安全的。没有不必要的资源被继承。
在支持通过Unix管道传递文件描述符的Unix平台上可用。
因此,我们的想法是"服务器进程"在安装新的之前继承默认的信号处理行为,因此它的所有子项也都有默认处理。
代码的全部荣耀:
from multiprocessing import Process, set_start_method
import sys
from signal import signal, SIGINT
from time import sleep
class NormalWorker(Process):
def run(self):
while True:
print('%d %s work' % (self.pid, type(self).__name__))
sleep(1)
class SpawningWorker(Process):
def handle_signal(self, signum, frame):
print('%d %s handling signal %r' % (
self.pid, type(self).__name__, signum))
def run(self):
signal(SIGINT, self.handle_signal)
sub = NormalWorker()
sub.start()
print('%d joining %d' % (self.pid, sub.pid))
sub.join()
print('%d %s joined sub worker' % (self.pid, type(self).__name__))
def main():
set_start_method('forkserver')
processes = [SpawningWorker() for ii in range(5)]
for pp in processes:
pp.start()
def sig_handler(signum, frame):
print('main handling signal %d' % signum)
for pp in processes:
pp.join()
print('main out')
sys.exit()
signal(SIGINT, sig_handler)
while True:
sleep(1.0)
if __name__ == '__main__':
main()
答案 2 :(得分:1)
您可以存储主进程的pid(在注册信号处理程序时),并在信号处理程序内使用它来路由执行流:
if os.getpid() != main_pid:
sys.exit(128 + signum)