标签:引用计数 术语 释放 控制线 运行 class 计数 通过 code
GIL,中文译为全局解释器锁。在讲解 GIL 之前,首先通过一个例子来直观感受一下 GIL 在 Python 多线程程序运行的影响。
首先运行如下程序:
import time start = time.clock() def CountDown(n): while n > 0: n -= 1 CountDown(100000) print("Time used:",(time.clock() - start))
运行结果为:
Time used: 0.0039529000000000005
在我们的印象中,使用多个(适量)线程是可以加快程序运行效率的,因此可以尝试将上面程序改成如下方式:
import time from threading import Thread start = time.clock() def CountDown(n): while n > 0: n -= 1 t1 = Thread(target=CountDown, args=[100000 // 2]) t2 = Thread(target=CountDown, args=[100000 // 2]) t1.start() t2.start() t1.join() t2.join() print("Time used:",(time.clock() - start))
运行结果为:
Time used: 0.006673
可以看到,此程序中使用了 2 个线程来执行和上面代码相同的工作,但从输出结果中可以看到,运行效率非但没有提高,反而降低了。
如果使用更多线程进行尝试,会发现其运行效率和 2 个线程效率几乎一样(本机器测试使用 4 个线程,其执行效率约为 0.005)。这里不再给出具体测试代码,有兴趣的读者可自行测试。
是不是和你猜想的结果不一样?事实上,得到这样的结果是肯定的,因为 GIL 限制了 Python 多线程的性能不会像我们预期的那样。
那么,什么是 GIL 呢?GIL 是最流程的 CPython 解释器(平常称为 Python)中的一个技术术语,中文译为全局解释器锁,其本质上类似操作系统的 Mutex。GIL 的功能是:在 CPython 解释器中执行的每一个 Python 线程,都会先锁住自己,以阻止别的线程执行。
当然,CPython 不可能容忍一个线程一直独占解释器,它会轮流执行 Python 线程。这样一来,用户看到的就是“伪”并行,即 Python 线程在交替执行,来模拟真正并行的线程。
有读者可能会问,既然 CPython 能控制线程伪并行,为什么还需要 GIL 呢?其实,这和 CPython 的底层内存管理有关。
CPython 使用引用计数来管理内容,所有 Python 脚本中创建的实例,都会配备一个引用计数,来记录有多少个指针来指向它。当实例的引用计数的值为 0 时,会自动释放其所占的内存。
举个例子,看如下代码:
>>> import sys >>> a = [] >>> b = a >>> sys.getrefcount(a) 3
可以看到,a 的引用计数值为 3,因为有 a、b 和作为参数传递的 getrefcount 都引用了一个空列表。
假设有两个 Python 线程同时引用 a,那么双方就都会尝试操作该数据,很有可能造成引用计数的条件竞争,导致引用计数只增加 1(实际应增加 2),这造成的后果是,当第一个线程结束时,会把引用计数减少 1,此时可能已经达到释放内存的条件(引用计数为 0),当第 2 个线程再次视图访问 a 时,就无法找到有效的内存了。
所以,CPython 引进 GIL,可以最大程度上规避类似内存管理这样复杂的竞争风险问题。
标签:引用计数 术语 释放 控制线 运行 class 计数 通过 code
原文地址:https://www.cnblogs.com/yuzemin/p/12815424.html