当一个线程忙于任务时所有线程都挂起
我有一个多线程的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 个回答
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
等)。