多进程池中偶尔发生死锁问题

7

我有 N 个独立的任务,在大小为 os.cpu_count()multiprocessing.Pool 中执行(在我的情况下是 8),并使用 maxtasksperchild=1(即每个新任务都创建一个新的工作进程)。

主脚本可以简化为:

import subprocess as sp
import multiprocessing as mp

def do_work(task: dict) -> dict:
    res = {}
    
    # ... work ...
   
    for i in range(5):
        out = sp.run(cmd, stdout=sp.PIPE, stderr=sp.PIPE, check=False, timeout=60)
        res[i] = out.stdout.decode('utf-8')

    # ... some more work ...

    return res


if __name__ == '__main__':
    tasks = load_tasks_from_file(...) # list of dicts

    logger = mp.get_logger()
    results = []

    with mp.Pool(processes=os.cpu_count(), maxtasksperchild=1) as pool:
        for i, res in enumerate(pool.imap_unordered(do_work, tasks), start=1):
            results.append(res)
            logger.info('PROGRESS: %3d/%3d', i, len(tasks))

    dump_results_to_file(results)

有时进程池会卡住。当我执行KeyboardInterrupt时,出现的回溯信息在这里。 它表明进程池无法获取新任务,和/或工作进程在队列/管道的recv()调用中卡住了。我无法确定地复现这个问题,在实验中尝试不同的配置后仍然无法复现。如果我再次运行相同的代码,则有机会优雅地完成。

进一步观察:

  • Python 3.7.9 在 x64 Linux 上
  • 使用 fork 作为多进程的启动方法(使用spawn不能解决此问题)
  • strace 显示进程被卡在一个 futex wait 上;gdb的回溯也显示:do_futex_wait.constprop
  • 禁用日志记录/显式刷新并没有帮助
  • 任务的定义中没有错误(即它们都可加载)。

更新:即使使用大小为1的进程池也会发生死锁。

strace 报告该进程被阻塞在尝试获取位于 0x564c5dbcd000 处的某个锁上:

futex(0x564c5dbcd000, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 0, NULL, FUTEX_BITSET_MATCH_ANY

并且gdb确认:

(gdb) bt
#0  0x00007fcb16f5d014 in do_futex_wait.constprop () from /usr/lib/libpthread.so.0
#1  0x00007fcb16f5d118 in __new_sem_wait_slow.constprop.0 () from /usr/lib/libpthread.so.0
#2  0x0000564c5cec4ad9 in PyThread_acquire_lock_timed (lock=0x564c5dbcd000, microseconds=-1, intr_flag=0)
    at /tmp/build/80754af9/python_1598874792229/work/Python/thread_pthread.h:372
#3  0x0000564c5ce4d9e2 in _enter_buffered_busy (self=self@entry=0x7fcafe1e7e90)
    at /tmp/build/80754af9/python_1598874792229/work/Modules/_io/bufferedio.c:282
#4  0x0000564c5cf50a7e in _io_BufferedWriter_write_impl.isra.2 (self=0x7fcafe1e7e90)
    at /tmp/build/80754af9/python_1598874792229/work/Modules/_io/bufferedio.c:1929
#5  _io_BufferedWriter_write (self=0x7fcafe1e7e90, arg=<optimized out>)
    at /tmp/build/80754af9/python_1598874792229/work/Modules/_io/clinic/bufferedio.c.h:396

1
你确定 subprocess.run 调用会完成吗?它会一直阻塞直到子进程结束,所以如果没有...... - bnaecker
1
你正在使用哪个操作系统和Python版本?各种选项的默认启动方法有所不同(在3.8之前,macOS使用fork,但是这引发了问题,所以3.8及更高版本默认切换到spawn),这可能与你的问题有关。即使在一个分叉系统上,你可能也想改变启动方法;如果父进程很大而子进程执行垃圾回收(导致写时复制,使内存使用量飙升),fork可能会成为一个问题。 - ShadowRanger
1
你能尝试将进程限制为1,看看在大任务列表的情况下是否仍然出现死锁吗? - Dschoni
似乎死锁也会发生在1个进程中... - Alexandru Dinu
1
如果不使用maxtasksperchild无法解决您的问题,您可以尝试升级到Python 3.8,因为有关于工作进程管理的更改。 - Darkonaut
显示剩余4条评论
1个回答

7

有没有更好的解决方案来避免这种情况? - Alberto Sinigaglia
这取决于你所说的“更好”的含义。如果你正在处理大量数据,你可以考虑在那个层面上进行潜在的优化(即修复原因而不是效果)。在我的情况下,主要问题是错误是“静默”的,因此切换到ProcessPoolExecutor有助于发现OOM行为。 - Alexandru Dinu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接