当一个线程忙于任务时所有线程都挂起

1 投票
1 回答
2007 浏览
提问于 2025-04-16 18:53

我有一个多线程的Python应用程序,里面有多个线程同时执行不同的任务。这个应用程序运行得很好已经有几个月了,但最近我遇到了一些问题。

其中一个线程启动了一个Python的 subprocess.Popen 对象,用来执行一个耗时的数据复制命令。

copy = subprocess.Popen(cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, preexec_fn = os.setsid, shell = False, close_fds = True)
if copy.wait():
  raise Exception("Unable to copy!")

在复制命令运行的时候,整个应用程序变得很慢,其他线程有时会几分钟都不运行。一旦 copy 完成,所有的事情又会恢复到之前的状态。

我正在尝试找出如何防止这种情况发生。我现在的最佳猜测是,这和我的操作系统内核调度进程的方式有关。我尝试添加 setsid() 来让复制进程和主Python应用程序分开调度,但这似乎没有效果。

我猜测 copy.wait() 函数的作用就是执行一个 waitpid()。有可能这个调用需要很长时间,而在这段时间里,那个线程占用了全局解释器锁(GIL)吗?如果是这样,我该如何防止或处理这个问题?我还能做些什么来进一步调试这个问题呢?

1 个回答

2

copy.wait() 持有全局解释器锁(GIL)是我最初的怀疑。不过,在我的系统上似乎并不是这样(wait() 调用并没有阻止其他线程的运行)。

你说得对,copy.wait() 最终会调用 os.waitpid()。在我的Linux系统上,它的样子是这样的:

PyDoc_STRVAR(posix_waitpid__doc__,
"waitpid(pid, options) -> (pid, status)\n\n\
Wait for completion of a given child process.");

static PyObject *
posix_waitpid(PyObject *self, PyObject *args)
{
    pid_t pid;
    int options;
    WAIT_TYPE status;
    WAIT_STATUS_INT(status) = 0;

    if (!PyArg_ParseTuple(args, PARSE_PID "i:waitpid", &pid, &options))
        return NULL;
    Py_BEGIN_ALLOW_THREADS
    pid = waitpid(pid, &status, options);
    Py_END_ALLOW_THREADS
    if (pid == -1)
        return posix_error();

    return Py_BuildValue("Ni", PyLong_FromPid(pid), WAIT_STATUS_INT(status));
}

这明显在POSIX的 waitpid 阻塞时释放了GIL。

我建议在程序卡住的时候,使用 gdb 连接到 python 进程,看看线程在干什么。也许这样能给你一些启发。

编辑 这是在 gdb 中一个多线程Python进程的样子:

(gdb) info threads
  11 Thread 0x7f82c6462700 (LWP 30865)  0x00007f82c7676b50 in sem_wait () from /lib/libpthread.so.0
  10 Thread 0x7f82c5c61700 (LWP 30866)  0x00007f82c7676b50 in sem_wait () from /lib/libpthread.so.0
  9 Thread 0x7f82c5460700 (LWP 30867)  0x00007f82c7676b50 in sem_wait () from /lib/libpthread.so.0
  8 Thread 0x7f82c4c5f700 (LWP 30868)  0x00007f82c7676b50 in sem_wait () from /lib/libpthread.so.0
  7 Thread 0x7f82c445e700 (LWP 30869)  0x00000000004a3c37 in PyEval_EvalFrameEx ()
  6 Thread 0x7f82c3c5d700 (LWP 30870)  0x00007f82c7676dcd in sem_post () from /lib/libpthread.so.0
  5 Thread 0x7f82c345c700 (LWP 30871)  0x00007f82c7676b50 in sem_wait () from /lib/libpthread.so.0
  4 Thread 0x7f82c2c5b700 (LWP 30872)  0x00007f82c7676b50 in sem_wait () from /lib/libpthread.so.0
  3 Thread 0x7f82c245a700 (LWP 30873)  0x00007f82c7676b50 in sem_wait () from /lib/libpthread.so.0
  2 Thread 0x7f82c1c59700 (LWP 30874)  0x00007f82c7676b50 in sem_wait () from /lib/libpthread.so.0
* 1 Thread 0x7f82c7a7c700 (LWP 30864)  0x00007f82c7676b50 in sem_wait () from /lib/libpthread.so.0

在这里,除了两个线程,其他所有线程都在等待GIL。一个典型的堆栈跟踪是这样的:

(gdb) thread 11
[Switching to thread 11 (Thread 0x7f82c6462700 (LWP 30865))] #0  0x00007f82c7676b50 in sem_wait () from /lib/libpthread.so.0
(gdb) where
#0  0x00007f82c7676b50 in sem_wait () from /lib/libpthread.so.0
#1  0x00000000004d4498 in PyThread_acquire_lock ()
#2  0x00000000004a2f3f in PyEval_EvalFrameEx ()
#3  0x00000000004a9671 in PyEval_EvalCodeEx ()
...

你可以通过在Python代码中打印 hex(t.ident) 来识别每个线程,其中 t 是一个 threading.Thread 对象。在我的系统上,这与在 gdb 中看到的线程ID相匹配(如 0x7f82c6462700 等)。

撰写回答