Python Web学习笔记之GIL机制下的鸡肋多线程

为什么有人会说 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

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

是什么原因导致多线程不快反慢的呢?

原因就在于 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,虽然无法达到高并发利用多核的优势,我们还需要线程同步做一些其他的事情。

参考

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Jerry的SAP技术分享

使用JDK自带的工具jstack找出造成运行程序死锁的原因

Java多线程编程也是Java面试中经常考察的内容。刚接触Java多线程编程的朋友们,可能会不慎写出一些会导致死锁(deadlock)的应用出来。如何分析造成J...

1044
来自专栏更流畅、简洁的软件开发方式

从吉日嘎拉那里学到的……

多个列表框联动,不算是啥大问题,但是却挺麻烦,那么怎么才能够尽量方便一点呢?网上搜了一下,没发现太好用的,于是就自己写了一个。基于jQuery,无限级联动,支持...

2046
来自专栏jeremy的技术点滴

Netty框架研究

2957
来自专栏非典型技术宅

Swift多线程之Operation:按优先级加载图片1. 进程和线程2. Operation3. Basic Demo4. 案例实现

1093
来自专栏向治洪

Universal-Image-Loader完全解析--从源代码分析Universal-Image-Loader中的线程池

一般来讲一个网络访问就需要App创建一个线程来执行,但是这也导致了当网络访问比较多的情况下,线程的数目可能积聚增多,虽然Android系统理论上说可以创建无数...

17410
来自专栏JAVA同学会

Zookeeper应用之——栅栏(barrier)

barrier的作用是所有的线程等待,知道某一时刻,锁释放,所有的线程同时执行。举一个生动的例子,比如跑步比赛,所有

743
来自专栏walterlv - 吕毅的博客

在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁

发布于 2018-03-23 13:54 更新于 2018-03...

471
来自专栏坚毅的PHP

java.util.concurrent 在shorturl项目中的应用

问题:微博短链项目应用到哪些concurrent包中的类,类的用途是什么?场景是怎样?效果? java.util.concurrent.atomic.Atomi...

3195
来自专栏智能大石头

线程池ThreadPool及Task调度机制分析

近1年,偶尔发生应用系统启动时某些操作超时的问题,特别在使用4核心Surface以后。笔记本和台式机比较少遇到,服务器则基本上没有遇到过。

680
来自专栏更流畅、简洁的软件开发方式

【分享】纯js的n级联动列表框 —— 基于jQuery,支持下拉列表框和列表框,最重要的是n级,当然还有更重要的

多个列表框联动,不算是啥大问题,但是却挺麻烦,那么怎么才能够尽量方便一点呢?网上搜了一下,没发现太好用的,于是就自己写了一个。基于jQuery,无限级联动,支持...

2448

扫码关注云+社区