Python动态多处理和信令问题

时间:2016-11-18 08:17:07

标签: python python-2.7 multiprocessing signals

我有自定义信号处理的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 永远应该是:在工作人员收到SIGINTSIGTERM之前。工人不应该立即戒烟;相反,它必须完成它所做的任何计算,然后退出(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)
                    )

问题

到目前为止基本设置,(几乎)一切正常:

  • 主进程产生所需数量的工作人员
  • 每个工作人员都连接到消息队列
  • 发布消息后,其中一名工作人员收到消息
  • Facade模式(使用名为 MessageRouter 的类)将收到的消息路由到相应的函数并执行它

现在出现问题:目标函数(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产生的进程也会将SIGINTSIGTERM的信号处理重定向到工作人员的handle_signal功能(因为信号传播)到过程子树),基本上打印Stopping worker (pid: ...)而不是停止。我知道,这是因为我在生成其子进程之前重新绑定了worker 的信号。

这就是我被困的地方:我无法设置工人'在产生子进程之后发出信号,因为我不知道它是否会生成一些(目标函数被屏蔽并且可能被其他人写入),并且因为工作者在其轮询中保留(按设计) -环。同时,我不能指望使用multiprocessing的目标函数的实现将其自己的信号处理程序重新绑定到(无论如何)默认值。

目前,我想在工作程序的每个循环中恢复信号处理程序(在将消息路由到其目标函数之前)并在函数返回后重置它们是唯一的选择,但它只是感觉不对。

我错过了什么吗?你有什么建议吗?如果有人能给我一个如何解决我的设计缺陷的暗示,我真的很开心!

3 个答案:

答案 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.Poolconcurrent.futures.ProcessPoolExecutor的工作人员管理。如果我需要更高级的功能,例如超时支持,我可以使用billiardpebble

每个工作人员将按照建议here忽略SIGINT。 SIGTERM保留为默认值。

服务

该服务由systemd或supervisord控制。在任何一种情况下,我都要确保终止请求始终作为SIGINT(CTL + C)传递。

我希望将SIGTERM作为紧急关闭而不是仅依靠SIGKILL。 SIGKILL不可移植,有些平台不实现它。

“我觉得这很简单”

如果事情更复杂,我会考虑使用LuigiCelery等框架。

总的来说,重新发明这些东西是非常有害的,并且给予很少的满足感。特别是如果其他人必须查看该代码。

如果你的目的是要了解这些事情是如何完成的,那么后一句不适用。

答案 1 :(得分:2)

我能够使用Python 3和set_start_method(method)使用'forkserver' flavour执行此操作。 Python 3的另一种方式> Python 2!

"这个"我的意思是:

  1. 有一个主进程,它有自己的信号处理程序,只是加入了孩子们。
  2. 让一些工作进程使用可能生成的信号处理程序......
  3. 具有信号处理程序的其他子进程。
  4. 然后在Ctrl-C上执行以下操作:

    1. 经理人流程等待工人退出。
    2. 工作人员运行他们的信号处理程序,(可能设置一个stop标志并继续执行完成他们的工作,虽然我没有在我的例子中烦恼,我刚刚加入了我认识的孩子)然后退出。
    3. 工人的所有孩子立即死亡。
    4. 当然请注意,如果您打算让工作人员的孩子不要崩溃,那么您需要在工作进程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)