防止多进程Python应用程序中的线程重复

时间:2014-03-19 15:27:50

标签: python multithreading multiprocessing python-multithreading

我有一个Python应用程序,它在multiprocessing.Process启动的子流程中运行多个作业。父应用程序还会启动一个线程来向数据库报告进度。但是,我注意到,如果任何作业启动自己的子进程,它们会复制此线程,从而导致数据库中的数据损坏。例如当一个子孙子进程完成时,它的线程将父作业标记为在数据库中完成,因为它认为它是 父进程,即使父进程仍在运行。

我如何使用multiprocess.Process以便它不会复制任何当前正在运行的线程?是最简单的选项,在我的线程中记录原始PID,如果“当前”PID与此值不匹配,那么立即退出?

我在去年看到了similar question,但它似乎被忽略了。

1 个答案:

答案 0 :(得分:2)

您对问题的描述表明父进程中的后台线程继续存在并在子进程中执行。那是不可能的;至少,在POSIX系统上是不可能的。在你的情况下发生的事情是别的。我将在下面提供一些猜测,然后提出如何避免这个问题的建议。反过来考虑这些要点......

<强> 1。只有一个线程可以继续分叉。

在分叉后,只有调用fork()的线程仍然存在。这是一个小例子,证明其他线程不会在子进程中继续执行:

def output():
    time.sleep(3)
    print "Thread executing in process: %d" % os.getpid()

thread = threading.Thread(target=output)
thread.start()
os.fork()
print "Pid: %d" % os.getpid()

你会看到父和子都将他们的pid打印到stdout,但是第二个线程只会在父节点中产生输出。

让监视器线程检查其pid或以其他方式判断它运行的进程不会产生影响;该线程只在一个进程中执行。

<强> 2。分叉的一些方法可能会导致您所看到的问题。

分叉可能会以各种方式导致程序状态损坏。例如:

  • 由于分叉而死亡的线程中引用的对象可能超出范围,因此会调用其终结器。例如,如果此类对象表示网络资源并且其del方法的调用导致连接的一端意外关闭,则会导致问题。
  • 任何缓冲的IO都会导致问题,因为缓冲区在子进程中是重复的。

请注意,第二点甚至不需要线程。请考虑以下事项:

f = open("testfile", "w", 1024)
f.write("a")
os.fork()

我们给testfile写了一个字符,我们在分叉之前在父母那里写过。但是,虽然内容仍然没有内容,但我们分道扬,所以:

alp:~ $ wc -c testfile
      2 testfile

该文件包含两个字符,因为输出缓冲区已复制到子级,父级和子级最终都刷新了缓冲区。

我怀疑你的问题是由第二个问题引起的(虽然我很高兴地承认这是纯粹的推测)。

第3。重新设计以避免此类问题。

您在评论中提到,在生成工作人员之后无法启动监视器线程,因为您需要重复创建新工作程序。可能比你想象的更容易重构你正在做的事情,以避免这种情况。不是为每个新工作单元生成一个进程,而是创建一组由控制进程管理的长期工作者:控制器提供一个队列,其中包含需要处理的作业的规范;它在休闲时这样做。每个工作程序无限循环,在队列到达并执行时从队列中绘制作业。 (来自multiprocessing的队列实现将保证每个作业描述仅由一个工作者绘制。)因此,您只需要在早期生成一次工作者,并且可以在所有分支完成后创建监视器线程。

以下是这种组织的示意图:

from multiprocessing import Process, Queue

def work(q):
    while True:
        job = q.get()
        if job is None:
            # We've been signaled to stop.
            break
        do_something_with(job)

queue = Queue()
NUM_WORKERS = 3
NUM_JOBS = 20

# Start workers.
for _ in range(NUM_WORKERS):
    p = Process(target=work, args=(queue,))
    p.start()

# Create your monitor thread here.

# Put work in the queue.  This continues as long as you want.
for i in range(NUM_JOBS):
    queue.put(i)

# When there's no more work, put sentinel values in the queue so workers
# know to gracefully exit.
for _ in range(NUM_WORKERS):
    queue.put(None)