多解释器多线程场景下PyThreadState_SetAsyncExc无效问题排查
我来帮你拆解这个问题,你的代码里有几个核心问题导致PyThreadState_SetAsyncExc返回1却无法终止目标线程:
1. 错误的线程ID被传入PyThreadState_SetAsyncExc
这是最关键的问题:你在stop()函数里使用的this->tstate->thread_id,并不是你启动的工作线程的ID!
看你的Script构造函数:
Script() { PyThreadState *ts = PyThreadState_Get(); this->tstate = Py_NewInterpreter(); PyThreadState_Swap(ts); }
这里的tstate是在主线程中创建的新解释器对应的线程状态,它的thread_id是主线程的操作系统线程ID,而不是run_thread启动的那个执行Python代码的工作线程ID。
而你的工作线程是在thread_func里通过PyThreadState_New(this->tstate->interp)创建了属于自己的线程状态,这个新的线程状态才对应工作线程的ID,但你没有把这个ID保存下来供stop()使用。
PyThreadState_SetAsyncExc的第一个参数需要的是目标工作线程的原生操作系统线程ID,你传入主线程ID,函数返回1只是因为主线程确实存在,但异常被投递到了主线程,而不是你想终止的那个执行Python循环的线程。
2. 异步异常的触发时机限制(次要但需要注意)
即使你传对了线程ID,还要注意:Python的异步异常只有当目标线程持有GIL并处于Python字节码执行循环中时才会被检查和触发。你的Python代码里有time.sleep(1),这个函数在底层会释放GIL并进入系统级睡眠,此时线程不会检查异步异常,只有当它从sleep返回、重新获取GIL后,才会处理你设置的异常。不过这不是你当前的主要问题,主要问题还是线程ID错误。
修复方案的关键点
要解决这个问题,你需要:
- 在
Script结构体中添加一个成员变量来保存工作线程的thread_id,比如unsigned long work_thread_id; - 在
thread_func中创建完工作线程的PyThreadState后,把这个ID保存下来:void thread_func(const char *code) { PyThreadState *ts = PyThreadState_New(this->tstate->interp); this->work_thread_id = ts->thread_id; // 保存工作线程ID PyEval_RestoreThread(ts); // ... 其余代码不变 } - 在
stop()函数中使用保存的work_thread_id来调用PyThreadState_SetAsyncExc:void stop() { PyThreadState *ts = PyThreadState_Swap(this->tstate); PyObject *exc = PyObject_CallObject(PyExc_Exception, Py_BuildValue("(s)", "stopped")); int n = PyThreadState_SetAsyncExc(this->work_thread_id, exc); // 用正确的线程ID if(n < 1) std::cerr << "Script::stop: thread not found!" << std::endl; PyThreadState_Swap(ts); }
这样修改后,PyThreadState_SetAsyncExc才会把异常投递到正确的工作线程,当线程从time.sleep返回后,就会触发异常并终止循环。
内容的提问来源于stack exchange,提问作者fferri

