Python新手引导第二篇

刚接触 Python 的时候就听到了 GIL 这个词,同时发现这个词经常和 Python 无法高效的实现多线程划上等号。

GIL 是啥子东西

先声明下 GIL 其实并不是 Python 语言的特性,它其实是在实现 Python 解释器(CPython)时所引入的一个概念。

那么,CPython 又是什么呢? 类比 C++,这是一套语言的标准,但是可以再不同的编译器上来编译成可执行代码。比较有名的编译器例如 GCC, Visual C++等。Python 也一样,相同的代码可以通过 CPython、PyPy、Psyco、JPython等不同的 Python 环境来执行。比如 JPython 就没有 GIL。

因为 CPython 是大部分环境下默认的 Python 执行环境,所以很多人在概念上就默认 CPython 就是 Python,也就想当然的把 GIL 作为了 Python 语言的缺陷。所以我这里再次明确一点: GIL 并不是 Python 的特性, Python 完全可以不依赖于 GIL。

那么 CPython 中的 GIL 又是什么呢?

全称是 Global Interpreter Lock。

官网是这么解释的:

一个防止多线程并发执行机器码的一个 Mutex。

尼玛啊,这不就是一个 Bug 般存在的全局锁嘛!表急。。。

为什么会有 GIL

众所周知,CPU 厂商在核心频率上的发展已经被多核心所替代。那么,为了更有效的利用多核处理器的性能,也就出现了多线程的编程方式,随之而来的也即是线程间数据一致性和状态同步的问题。

同样的,Python 也逃不开,为了利用多核 Python 开始支持多线程。而为了解决多线程之间数据完整性和状态同步的最简单、直接的方法自然就是加锁。于是也就有了 GIL 这一把大锁,更可怕的是这种特性被越来越多的代码库开发者接受,并且大量依赖这个特性。

当代码库越来越多的依赖这个特性之后,才发现这是多么的蛋疼和低效。但,当大家开始要去拆分和去除 GIL 的时候,发现大量代码库开发者已经重度依赖 GIL 而且非常难以去除了。

所以,简单的说 GIL 的存在更多的是历史原因。如果非要推倒重来,多线程的问题依然还是要面对的,但是我想,至少会比目前 GIL 这种方式会更加优雅点。

证明 GIL 的低效

GIL 无疑就是一把全局排它锁,毫无疑问全局锁的存在会对多线程的效率有不小的影响。甚至可以认为 Python 特么的就是一个单线程的语言。为什么说它是一个单线程,有依据的,更扯淡的是它还有可能会比单线程的效率都差。

来个大众的例子,一个循环 1 亿次的计数器函数,通过一个单线程执行两次和一个多线程同时执行一次。我们来看看耗时:

为了减少线程库本身性能损耗对测试结果带来的影响,这里单线程的代码同样用到了线程,只不过顺序执行两次来模拟单线程。

  • 顺序执行的单线程(single_thread.py)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from threading import Thread
import time
def my_counter():
i = 0
for _ in range(100000000):
i = i + 1
return True
def main():
thread_array = {}
start_time = time.time()
for tid in range(2):
t = Thread(target=my_counter)
t.start()
t.join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()
  • 同时执行的两个并发线程(multi_thread.py)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from threading import Thread
import time
def my_counter():
i = 0
for _ in range(100000000):
i = i + 1
return True
def main():
thread_array = {}
start_time = time.time()
for tid in range(2):
t = Thread(target=my_counter)
t.start()
thread_array[tid] = t
for i in range(2):
thread_array[i].join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()

测试结果: