Python - 多处理队列在子进程被杀死后没有以正确的顺序返回结果

时间:2015-06-22 11:46:38

标签: python subprocess python-multiprocessing

我正在迭代规则列表(每个规则都是一个很大的布尔表达式)。 我打算用Pyeda库来解决这个表达式。步骤是1.解析规则,2。转换为BDD形式,3。解决规则。步骤对于我所面临的问题并不重要,并且包含在do_big_job函数中,该函数需要rule来解决,而global Queue (q)来自`multiprocessing.Manager',而不是一般的Queue。

我必须超时处理太长时间('time_in'秒)的规则。 do_threading函数采用全局q (Queue),要在子进程中运行的函数(do_big_job)和要传递给do_big_job和{{的参数(规则) 1}}来控制子进程的执行。

令我惊讶的是,我观察到当出现超时并且子进程因运行时间过长而被终止时,结果就会出现故障,即队列中返回的值与规则不匹配通过并属于其他早期规则。

我在这里做错了什么? 有没有其他方法可以做我想做的事情?

另外,我还有一个问题,当我以线性方式执行此操作而不使用多处理时,处理每个规则需要花费更多时间,而不是在单独的进程中处理每个规则。可能有什么解释呢?

timeout_in

2 个答案:

答案 0 :(得分:1)

如果您无法控制处理每个规则的所有代码,那么单独的Process是实现超时的可靠解决方案。

您可以将规则添加到结果中,以避免担心订单。为避免破坏公共队列,可以为每个进程使用单独的管道(未测试):

#!/usr/bin/env python3
from itertools import islice
from multiprocessing import Process, Pipe, cpu_count, freeze_support
from multiprocessing.connection import wait

def do_big_job(rule, conn):
    with conn:
        # compute solution, count for the rule..
        # send the result to the parent process
        conn.send((rule, solution, count))

def main():
    jobs = {} # mapping: connection -> process
    max_workers = cpu_count() # max number of concurrent jobs
    rules = iter(rules) # make an iterator

    for rule in islice(rules, max_workers): # start initial jobs
        add_job(jobs, rule)

    while jobs:
        ready = wait(jobs, timeout)
        if not ready: # timeout and no results are ready
           rotate_job(jobs, rules) # remove old job, add a new one

        for conn in ready: # get results
            try:
                rule, solution, count = conn.recv()
            except EOFError:
                rotate_job(jobs, rules, conn)        
            else:
                print(rule, solution, count)

if __name__ == '__main__':
    freeze_support()
    main()

其中add_job()rotate_job()实现了一个限制并发进程数并允许终止其工作进程的进程池:

def add_job(jobs, rule): #XXX create class for the Pool
    r, w = Pipe(duplex=False)
    with w:
        p = Process(target=do_big_job, args=[rule, w], daemon=True)
        p.start() 
    jobs[r] = p

def rotate_job(jobs, rules, conn=None): 
    if conn is None:
       for conn in jobs:
           break

    # start next job
    for rule in rules:
        add_job(jobs, rule)
        break

    # remove timeouted job
    process = jobs.pop(conn)
    if process.is_alive():
        process.terminate()
    process.join() #NOTE: it may hang if `.terminate()` is ignored

池实现为每个作业创建一个新的Process

答案 1 :(得分:1)

由于各种原因,我不得不对您的代码进行大量更改 有些名称的定义不像part_num 我省略了使用实际的Pyeda库。多处理的解决方案是通用的,工作进程中实际发生的事情与处理进程之间的数据流无关 我也没有尝试猜测从哪里导入expr 因此,某些函数被模拟,但它们与理解并行计算无关 模拟被相应地评论,因此是虚拟输入数据。

您的代码的主要问题是您希望启动工作程序并在一个循环中收集结果。每当您使用线程或多处理时忘记它,因为从工作者返回的数据顺序基本上是未定义的。因此,工作人员有责任提供有关其正在处理的规则以及结果的明确信息。

另一个很大的区别是,我实际上在开始时启动所有工作人员,这使得计算实际上是平行的。然后我收集传入的结果。每当队列出现时,我都会检查所有工作人员是否已经返回了他们的退出代码,这是一个明确的信息,表明不再有任何有趣的事情发生了。

主进程不负责超时工作人员。工作人员在超时SIGALRM后终止自身。我是这样做的,因为master进程没有关于工作进程何时进入Python代码入口点的可靠信息。

最后一件事是我根据timed_out_parts中缺少的结果填充solved_parts

from multiprocessing import Process, Manager
from multiprocessing.queues import Empty as QueueEmpty
from signal import alarm

# Following imports are only needed to mock some function
from time import sleep
from collections import namedtuple
import random

# Mock for `expr()`
def expr(rule):
    return rule

# Mock for `expr2bdd()` - sleeps randomly simulating heavy computation
def expr2bdd(expression):
    sleep(random.randint(0, 9))
    satisfied = [n for n in xrange(random.randint(0, 5))]
    def satisfy_count():
        return len(satisfied)
    def satisfy_all():
        return satisfied
    Evaluation = namedtuple('Evaluation', ('satisfy_count', 'satisfy_all'))
    return Evaluation(satisfy_count=satisfy_count, satisfy_all=satisfy_all)

# Mock for `count_unique_variables()`
def count_unique_variables(arg):
    return random.randint(0, 9)

# This function is executed in separate process - does the actual computation
def evaluate_rule(queue, part_num, rule, timeout):
    alarm(timeout)
    print 'Part: {}, Rule: {}'.format(part_num, rule)
    evaluation = expr2bdd(expr(rule))
    count = evaluation.satisfy_count()
    solution=[]
    for i in evaluation.satisfy_all():
        solution.append(i)
    queue.put([part_num, solution, count])

# Main function which starts workers and collects results
def evaluate_rules(rules, timeout=5):
    manager = Manager()
    queue = manager.Queue()
    solved_parts = {}
    processes = []
    for part_num, rule in enumerate(rules):
        process = Process(target=evaluate_rule, args=(queue, part_num, rule, timeout))
        process.start()
        processes.append(process)
    while True:
        try:
            result = queue.get_nowait()
        except QueueEmpty:
            if all((process.exitcode is not None for process in processes)):
                break
        solved_parts[result[0]] = {
            'solution': result[1],
            'solution_count': result[2],
            'count_unique_var': count_unique_variables(rule)
        }
    timed_out_parts = {
        part_num: {
            'solution': None,
            'solution_count': None,
            'count_unique_var': count_unique_variables(rule)
        }
        for part_num, rule in enumerate(rules) if part_num not in solved_parts
    }
    return solved_parts, timed_out_parts

# Initialize `random generator` - only for mocks
random.seed()

# Dummy rules
rules = [i for i in xrange(50)]

# Fun starts here
solved_parts, timed_out_parts = evaluate_rules(rules)

# You definitely want to do something more clever with the results than just printing them
print solved_parts
print timed_out_parts

关于你的第二个问题:没有更好的答案。线性和并行处理时间的差异取决于工人实际做了什么。