1. 什么是GIL
Python作为一种解释型动态语言,相信大家都知道它的便捷和方便,但它存在一个严重的问题-GIL(Global Interpreter Lock)。GIL 相当于Python解释器的一个全局锁,它保证同一时刻只有一个线程能够执行 Python 代码。也就是说,即使你有多个线程,但是同一时刻只有一个线程的状态是活动的。
GIL并不是Python语言本身的问题,它是在实现Python解释器(CPython)时添加的,实现的目的是为了保护解释器中的内存管理机制。
那么GIL为什么会影响多线程并发呢?因为同一时刻只有一个线程能够执行Python代码,所以多个线程在使用CPU时都无法完全使用CPU资源,这样就会出现“大杀器”Python变成了“小菜鸟”的现象。
2. GIL的实现机制
GIL全局锁的实现机制是基于线程的单向执行方式,即线程在执行的时候锁住全局资源,其他线程无法进入执行状态。由于GIL的存在,无论CPU的核心数目多少,Python的多线程执行模式都是错综复杂的。
2.1 GIL的锁机制
为了保证线程的安全,Python解释器实现了一个叫GIL的全局锁机制。在解释器中,一次只能有一个线程获取Python解释器的控制权,其他线程只能等待其中一个线程释放控制权后才能获取控制权。这种模型保证了在同一时刻只有一个线程执行至关重要的虚拟机代码区域。
在Python解释器中,为了让扩展模块不必关注线程安全性问题,所以在导入扩展模块时并不会释放GIL,这使得Python在使用扩展模块时更加保险。
2.2 GIL的释放方式
如果当前线程处在由GIL锁住的状态下,那么在 C 语言的执行时刻,该线程需要释放掉GIL。当一个线程睡眠或者 I/O 调用等待数据的时候,对应的线程必须释放 GIL。直到它再次准备恢复线程时,GIL才会重新加锁。这样就会使得其他线程可以获取并开始执行
3. 如何尽可能充分地利用CPU资源
尽管GIL是一个比较讨厌的锁,但是Python也提供了一些方式来尽可能地充分利用CPU资源。
3.1 使用多进程
如果需要在多CPU环境下使用Python的并行运算机制,那么使用多进程肯定是一个不错的选择。
from multiprocessing import Pool
# 计算函数
def fib(n):
if n<=2:
return 1
else:
return fib(n-1)+fib(n-2)
# 多进程计算Fibonacci序列
if __name__ == '__main__':
with Pool(processes=4) as pool:
print(pool.map(fib, [33]*4))
注意:使用多进程不会像多线程那样有GIL的制约,但是多进程之间的通信需要消耗一定的时间和资源。
3.2 C++扩展模块
使用C++语言编写Python扩展模块是另一个解决GIL锁定的方法。由于C++是一种底层语言,所以可以获得更高的性能,并且不受GIL限制。
#include
PyObject *demo_method(PyObject *self, PyObject *args) {
long long count = 0;
for (long long i = 0; i < 10000000000; i ++ ){
count += (i * i) % 3;
}
Py_RETURN_NONE;
}
static PyMethodDef demoMethods[] = {
{"demo", demo_method, METH_VARARGS, "The demo method"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef demoModule = {
PyModuleDef_HEAD_INIT,
"demo",
NULL,
-1,
demoMethods
};
PyMODINIT_FUNC PyInit_demo(void) {
PyObject* deref = PyModule_Create(&demoModule);
return deref;
}
3.3 使用threading模块中的lock对象
Python标准库中的threading模块为我们提供了Lock对象的支持,使用Lock对象可以控制对临界区的访问,从而实现对资源加锁的处理。
import threading
class CalcThread(threading.Thread):
def run(self):
global number
lock.acquire()
for _ in range(100000):
number += 1
lock.release()
if __name__ == "__main__":
lock = threading.Lock()
number = 0
threads = []
for i in range(10):
t = CalcThread()
threads.append(t)
t.start()
for t in threads:
t.join()
print(number)
4. 总结
Python GIL是一个影响Python多线程并发的问题,它影响多个线程可以协同的执行,导致不能预期地浪费CPU资源。但由于Python特定的语法和库提供了一些解决GIL问题的方法,例如使用多进程、C++扩展模块、锁和队列等,因此我们可以尽可能地利用Python进行并发编程。