全局解释器锁(GIL,Global Interpreter Lock) Python代码的执行由Python虚拟机(解释器)来控制。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同时只有一个线程在运行。所以就会出现尽管你设置了多线程的任务,但是只能跑一个的情况。
但是I/O密集的程序(爬虫)相对好一点,因为I/O操作会调用内建的操作系统C代码,所以这时会释放GIL锁,达到部分多线程的效果。
通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
多线程(鸡肋) 1 from threading import Thread
多进程(正常实现) 通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
1 from multiprocessing import Process
spawn: 会从头import模块,执行全局变量的内容,相对fork来说更独立安全。
子进程调用实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 def TIMEOUT_COMMAND (command, timeout=10 ): """call shell-command and either return its output or kill it if it doesn't normally exit within timeout seconds and return None""" import subprocess, datetime, os, time, signal cmd = command.split(" " ) start = datetime.datetime.now() process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,encoding="utf-8" ,preexec_fn=os.setsid) ic("BHive-noUseOSACA-before" ,process.pid,process.poll()) while process.poll() != 208 : ic("BHive-noUseOSACA-During" ,process.pid,process.poll()) time.sleep(0.2 ) now = datetime.datetime.now() if (now - start).seconds> timeout: os.killpg(os.getpgid(process.pid), signal.SIGKILL) (killPid,killSig) = os.waitpid(process.pid, 0 ) if killPid != process.pid or killSig!=9 : errorPrint("TIMEOUT_COMMAND kill failed! killPid %d process.pid %d killSig %d" % (killPid, process.pid, killSig)) ic("Killed" ,process.pid,process.poll()) return None ic("BHive-noUseOSACA-Finished" ,process.pid,process.poll()) return process.stdout.readlines()
使用Queue或者Pipe通讯 参考
调用C语言的链接库 把一些计算密集型任务用C语言编写,然后把.so链接库内容加载到Python中,因为执行C代码,GIL锁会释放,这样一来,就可以做到每个核都跑一个线程的目的!
类似PTA的代码中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 PyObject* THNPModule_attachOutOfMemoryObserver (PyObject* _unused, PyObject* observer) { HANDLE_TH_ERRORS Py_XINCREF (observer) ; auto obs = [observer](int64_t device, int64_t alloc, int64_t device_allocated, int64_t device_free) { py::gil_scoped_acquire g; PyObject* result = PyObject_CallFunction (observer, "LLLL" , device, alloc, device_allocated, device_free); if (!result) { throw py::error_already_set (); } Py_XDECREF (result); }; torch_npu::utils::npu_lazy_init (); c10_npu::NPUCachingAllocator::attachOutOfMemoryObserver (std::move (obs)); Py_RETURN_NONE; END_HANDLE_TH_ERRORS }
进程池
pybind11 / GIL in C++ Pybind11 中的 GIL 锁 Pybind11 是一个 C++ 库,用于方便地创建 Python 的 C++ 扩展模块。通过 Pybind11,你可以将 C++ 的函数和类暴露给 Python,使得 Python 能直接调用这些 C++ 的功能。它的设计目标是简化 Python 和现代 C++(C++11 及更高版本)之间的绑定工作,同时提供一个高效、直观的接口。
轻量级 :不需要复杂的配置,头文件即用(header-only)。
现代化 :支持 C++11/14/17 的特性,比如智能指针、模板和 lambda。
易用性 :极简的语法,几乎不需要写样板代码。
高性能 :生成的模块接近直接调用 C++ 的性能。
安装 Pybind11
如果尚未安装,可以通过 pip 直接安装:
或者通过包管理器安装(例如 Ubuntu 上):
1 sudo apt-get install pybind11-dev
创建一个简单的绑定
假设你有一个 C++ 文件 example.cpp
,它提供了一个简单的 C++ 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <pybind11/pybind11.h> namespace py = pybind11;int add (int a, int b) { return a + b; } PYBIND11_MODULE (example, m) { m.doc () = "Pybind11 example module" ; m.def ("add" , &add, "A function that adds two numbers" ); }
编译绑定代码
使用 Pybind11 提供的 pybind11-config
脚本获取编译选项:
1 c++ -O3 -Wall -shared -std=c++11 -fPIC `python3 -m pybind11 --includes` example.cpp -o example`python3-config --extension-suffix`
这里生成了 example.cpython-<version>-<platform>.so
文件,这是一个可以在 Python 中直接导入的模块。
在 Python 中使用
运行 Python,导入刚刚生成的模块并调用函数:
1 2 3 import exampleprint (example.add(3 , 5 ))
自动获取和释放 GIL 默认情况下,Pybind11 在调用 Python API 时会自动管理 GIL。如果你调用 Python 代码或与 Python 对象交互,Pybind11 会自动获取 GIL 并在调用结束后释放它。
1 2 3 4 5 6 7 8 9 10 11 12 #include <pybind11/pybind11.h> namespace py = pybind11;void example_function () { py::print ("Hello from C++!" ); } PYBIND11_MODULE (example, m) { m.def ("example_function" , &example_function); }
在这个简单的示例中,py::print
调用会自动获取 GIL,并在返回时释放它。这保证了多线程环境下的线程安全。
手动获取和释放 GIL 如果你的 C++ 代码中有与 Python 无关的操作,或者你想在执行耗时的 C++ 计算时释放 GIL,从而让其他 Python 线程可以执行,你可以手动控制 GIL。
Pybind11 提供了 py::gil_scoped_acquire
和 py::gil_scoped_release
来显式地获取和释放 GIL。
如果你需要调用 Python 代码(例如操作 Python 对象),你必须先获取 GIL。
1 2 3 4 void some_function () { py::gil_scoped_acquire acquire; py::print ("This requires GIL" ); }
如果你在 C++ 代码中执行计算密集型操作,可以释放 GIL,从而允许其他 Python 线程执行。在 py::gil_scoped_release
的作用域内,GIL 会被释放,允许其他线程在这个时间段执行 Python 代码。C++ 代码执行完后,GIL 会重新自动获取。
1 2 3 4 5 6 7 8 9 10 void long_running_task () { { py::gil_scoped_release release; for (int i = 0 ; i < 100000000 ; ++i) { } } }
PTA 代码中, 直接调用py::gil_scoped_release release;
时报错。
但是使用如下代码运行没有报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 #ifndef BUILD_LIBTORCH #include <Python.h> #endif PyThreadState *gilState = nullptr ; if (PyGILState_Check ()) { gilState = PyEval_SaveThread (); } if (gilState) { PyEval_RestoreThread (gilState); }
快捷语法:定义函数时释放 GIL py::call_guard<py::gil_scoped_release>()
确保了在调用 broadcast_coalesced
时,C++ 层的计算不会占用 GIL,从而允许其他 Python 线程继续执行。
1 2 3 4 5 6 7 8 9 10 11 m.def ( "_broadcast_coalesced" , [](std::vector<at::Tensor>& tensors, std::vector<int64_t > devices, size_t buffer_size) { return torch_npu::data_parallel::broadcast_coalesced (tensors, devices, buffer_size); }, py::arg ("tensors" ), py::arg ("devices" ), py::arg ("buffer_size" ), py::call_guard <py::gil_scoped_release>())
常见错误:释放锁后调用 释放 GIL后还是调用python代码
如果你在 C++ 代码中使用了 py::call_guard<py::gil_scoped_release>()
来释放 **GIL (Global Interpreter Lock)**,并且在释放 GIL 后又调用了 Python 代码(比如 py::print("GIL is re-acquired.")
),那么 会导致未定义行为 ,并且在大多数情况下会导致 崩溃 (通常是 segmentation fault 或其他类似错误)。原因如下:
GIL 被释放 :当你使用 py::call_guard<py::gil_scoped_release>()
时,GIL 会被释放,允许其他 Python 线程执行。这意味着 Python 解释器的锁被释放,其他线程可以访问 Python 对象和执行 Python 代码。
GIL 被释放后调用 Python 代码 :如果在释放 GIL 后,你再尝试调用 Python 代码(例如 py::print
),就会出现问题,因为没有 GIL 的保护,Python 解释器的线程安全机制会被破坏。Python 的 C API 并不是线程安全的,必须在有 GIL 的情况下才能调用 Python 代码,否则可能会出现以下几种问题:
Segmentation Fault :由于没有 GIL,Python 解释器内部的内存管理可能会出错,导致访问无效的内存位置,进而触发 segmentation fault 。
未定义行为 :因为没有 GIL 保护,Python 内部的对象和数据结构可能在多线程环境下发生竞争条件,导致不可预期的错误。
正确的做法 :如果你需要在释放 GIL 后执行 Python 代码,你应该重新获取 GIL。你可以通过 py::gil_scoped_acquire
来手动获取 GIL,然后安全地执行 Python 代码。这样做能够确保 Python 代码在执行时能安全地访问 Python 的内部数据结构。
以下是一个错误的示例(会导致崩溃或未定义行为):
1 2 3 4 5 6 7 8 void bad_example () { { py::gil_scoped_release release; py::print ("This will crash!" ); } }
正确的做法: 如果你必须在释放 GIL 后调用 Python 代码,你应该重新获取 GIL。例如:
1 2 3 4 5 6 7 8 9 10 11 void correct_example () { { py::gil_scoped_release release; } { py::gil_scoped_acquire acquire; py::print ("This is safe." ); } }
1 pybind11::handle::inc_ref() is being called while the GIL is either not held or invalid.
这时gdb的信息会显示python的cpython栈。
PTA在DEBUG模式下,GIL锁已释放,但是对象的引用计数还在增加,说明在没有正确持有 GIL 的情况下,尝试操作 Python 对象,导致引用计数操作失败。可能 会触发segfault。
但是通过编译选项-DNDEBUG
掩盖报错信息, 但是问题还是实际存在的,导致难以定位。
实例
check process create time
1 2 ps -eo pid,lstart,cmd |grep bhive date
kill all process by name
1 sudo ps -ef | grep 'bhive-re' | grep -v grep | awk '{print $2}' | sudo xargs -r kill -9
以为的原因
subProcess.pool 返回程序状态的时候,除了运行和结束状态,还有休眠等其他状态。也就是程序在发射之后并不是直接进入运行状态的。判断程序是否超时不能通过判断是否运行,因为一开始while循环进不去
1 while process.poll() is None :
而应该是判断是否正常结束(208是BHive结束返回值,不同程序不同)
1 while process.poll() != 208 :
继续分析
实际debug还是有
在debug输出里没有这些pid
check了,输出的个数是符合的。
不懂了,我都没调用,这僵尸进程哪里来的?除非是BHive产生的。
实际原因
调用的Bhive会产生子进程,原本的python实现不能杀死子进程的子进程。需要改用杀死进程组的实现
杀死进程组
可能设定是timeout是20秒,但是htop程序运行了2分钟也没有kill。这是正常的,因为主程序挤占资源导致挂起了,导致无法及时判断和kill
参考文献 无