为何有人说 Python 多线程是鸡肋?

为何有人会说 Python 多线程是鸡肋?知乎上有人提出这样一个问题,在咱们常识中,多进程、多线程都是经过并发的方式充分利用硬件资源提升程序的运行效率,怎么在 Python 中反而成了鸡肋?python

有同窗可能知道答案,由于 Python 中臭名昭著的 GIL,GIL 是什么?为何会有 GIL?多线程真的是鸡肋吗? GIL 能够去掉吗?带着这些问题,咱们一块儿往下看,同时须要你有一点点耐心。安全

多线程是否是鸡肋,咱们先作个实验,实验很是简单,就是将数字 “1亿” 递减,减到 0 程序就终止,这个任务若是咱们使用单线程来执行,完成时间会是多少?使用多线程又会是多少?show me the code服务器

# 任务
def decrement(n):
    while n > 0:
        n -= 1

单线程 数据结构

import time

start = time.time()
decrement(100000000)
cost = time.time() - start
>>> 6.541690826416016

在个人4核 CPU 计算机中,单线程所花的时间是 6.5 秒。可能有人会问,线程在哪里?其实任何程序运行时,默认都会有一个主线程在执行。(关于线程与进程这里不展开,我会单独开一篇文章)多线程

多线程 并发

import threading

start = time.time()

t1 = threading.Thread(target=decrement, args=[50000000])
t2 = threading.Thread(target=decrement, args=[50000000])

t1.start() # 启动线程,执行任务
t2.start() # 同上

t1.join() # 主线程阻塞,直到t1执行完成,主线程继续日后执行
t2.join() # 同上

cost = time.time() - start

>>>6.85541033744812

建立两个子线程 t一、t2,每一个线程各执行 5 千万次减操做,等两个线程都执行完后,主线程终止程序运行。结果,两个线程以合做的方式执行是 6.8 秒,反而变慢了。按理来讲,两个线程同时并行地运行在两个 CPU 之上,时间应该减半才对,如今不减反增。app

是什么缘由致使多线程不快反慢的呢?测试

缘由就在于 GIL ,在 Cpython 解释器(Python语言的主流解释器)中,有一把全局解释锁(Global Interpreter Lock),在解释器解释执行 Python 代码时,先要获得这把锁,意味着,任什么时候候只可能有一个线程在执行代码,其它线程要想得到 CPU 执行代码指令,就必须先得到这把锁,若是锁被其它线程占用了,那么该线程就只能等待,直到占有该锁的线程释放锁才有执行代码指令的可能。线程

所以,这也就是为何两个线程一块儿执行反而更加慢的缘由,由于同一时刻,只有一个线程在运行,其它线程只能等待,即便是多核CPU,也没办法让多个线程「并行」地同时执行代码,只能是交替执行,由于多线程涉及到上线文切换、锁机制处理(获取锁,释放锁等),因此,多线程执行不快反慢。设计

何时 GIL 被释放呢?

当一个线程遇到 I/O 任务时,将释放GIL。计算密集型(CPU-bound)线程执行 100 次解释器的计步(ticks)时(计步可粗略看做 Python 虚拟机的指令),也会释放 GIL。能够经过 sys.setcheckinterval()设置计步长度,sys.getcheckinterval() 查看计步长度。相比单线程,这些可能是多线程带来的额外开销

CPython 解释器为何要这样设计?

多线程是为了适应现代计算机硬件高速发展充分利用多核处理器的产物,经过多线程使得 CPU 资源能够被高效利用起来,Python 诞生于1991年,那时候硬件配置远没有今天这样豪华,如今一台普通服务器32核64G内存都不是什么司空见惯的事,可是多线程有个问题,怎么解决共享数据的同步、一致性问题,由于,对于多个线程访问共享数据时,可能有两个线程同时修改一个数据状况,若是没有合适的机制保证数据的一致性,那么程序最终致使异常,因此,Python之父就搞了个全局的线程锁,无论你数据有没有同步问题,反正一刀切,上个全局锁,保证数据安全。这也就是多线程鸡肋的缘由,由于它没有细粒度的控制数据的安全,而是用一种简单粗暴的方式来解决。

这种解决办法放在90年代,实际上是没什么问题的,毕竟,那时候的硬件配置还很简陋,单核 CPU 仍是主流,多线程的应用场景也很少,大部分时候仍是以单线程的方式运行,单线程不要涉及线程的上下文切换,效率反而比多线程更高(在多核环境下,不适用此规则)。因此,采用 GIL 的方式来保证数据的一致性和安全,未必不可取,至少在当时是一种成本很低的实现方式。

那么把 GIL 去掉可行吗?

还真有人这么干多,可是结果使人失望,在1999年Greg Stein 和Mark Hammond 两位哥们就建立了一个去掉 GIL 的 Python 分支,在全部可变数据结构上把 GIL 替换为更为细粒度的锁。然而,作过了基准测试以后,去掉GIL的 Python 在单线程条件下执行效率将近慢了2倍。

Python之父表示:基于以上的考虑,去掉GIL没有太大的价值而没必要花太多精力。

小结

CPython解释器提供了GIL(全局解释器锁)保证线程数据同步,那么有了 GIL,咱们还须要线程同步吗?多线程在IO密集型任务中,表现又怎样呢?欢迎你们留言