98、Python并发编程:Python的伪多线程、GIL以及自由线程特性

南宫理的日志录 2024-11-20 07:42:44
引言

我们介绍了在Python中实现多线程的两种方式,可以说在Python中要进行多线程编程,实现起来非常简洁。但是,稍微细心的你可能发现,Python中的多线程似乎并没有提高执行的效率,这又是为什么呢?本文就来进一步剖析一下Python中的多线程。

本文的主要内容有:

1、Python中多线程的执行效率

2、”万恶之源“的GIL

3、Python中的多线程还有用吗

4、Python3.13中的实验特性

Python中多线程的执行效率

首先我们通过数字的累加,来简单演示并比较一下单线程与多线程的实现的运行效率。

直接看代码:

代码中简单计算了从0一直累加到10^8的,分别通过单线程和多线程进行计算。

执行的结果如下:

从结果看,并没有节省多少时间,微乎其微……

有的同学可能觉得我的电脑是个老古董,只有一个单核CPU,所以,多线程最终落地只是实现了单核CPU的时间片轮转、分时服用,没有真正的并行。

但是,我的电脑是19年的,配置还行,CPU核心虽然不多,但也有8个呢

如果通过其他编程语言,比如Java,会有较大的差异。所以,Python中的多线程只是个假的”多线程“吗?

”万恶之源“的GIL

之所以出现多线程并没有有效提升执行的效率,有时候甚至会比单线程的执行效率还要慢,主要由于GIL的存在。

虽然标题中,我以”万恶之源“来形容GIL,但是,是打引号的。我们需要了解Python中GIL设计的由来,从而更好的理解Python中的多线程。Python的设计者,总不至于吃力不讨好,就是为了恶心我们这些开发者才添加的GIL吧。

为什么要有GIL呢?

有因必有果,GIL的存在自有其合理性。

在Python中,是通过引用计数来管理内存的,Python中创建的每一个对象都有一个引用计数的变量,该变量用来跟踪指向该对象的引用数量。先让,如果该变量被多个线程同时修改的话,会导致内存管理出现严重问题,要么是内存泄露(该释放的未能及时释放),要么是对象异常丢失(不该释放的被释放了)。

所以,引用技术变量必须是线程安全的。

要实现引用计数的线程安全,最粗暴的做法是直接给引用计数变量或者对象本身加锁。这样做的好处是对并发的影响最小,但是,一个不可回避的问题是死锁。此外,每个对象要不断申请锁、释放锁,对性能也会造成较大的影响。

如果不能粗暴的给每个对象的引用计数加锁,另一种能想到的做法就是,确保解释器进程中任意时刻只有一个线程对引用计数变量进行修改就可以了。但是,很难确定Python代码中的临界域,所以,最终这种的实现就是GIL了。

什么是GIL?

GIL,Global Interpreter Lock,也就是”全局解释器锁“。需要说明的是,并不是所有的Python实现中都有。GIL是CPython解释器中的一种机制。

GIL的存在,确保了任意时刻,只有一个线程执行Python字节码(可能导致引用计数的修改)。所以,Python,尤其是CPython中,多线程并不能实现并行计算,因为同一时刻,只有一个线程在执行。

需要补充说明的是:

1、每个线程需要执行Python字节码时,必须先获取GIL,如果当前线程没有GIL,线程将被阻塞,直到GIL可用。

2、为了确保并发的效率以及调度执行的公平性,GIL有相应的释放机制:

1)线程在进行IO操作时,不需要执行Python字节码了,这时候会自动释放GIL,所谓的IO操作,主要涉及到文件读写、网络通信等。

2)Python解释器还会定期释放GIL,一般是一定的时间间隔或者执行了一定数量的字节码指令,时间间隔可以通过sys.getswitchinterval()函数进行查看。

Python中的多线程还有用吗

既然Python解释器进程中,同一时刻只有一个线程会被执行,那么,我们是不是应该放弃Python中的多线程呢,直接使用多进程?

虽然存在GIL,导致多线程没法真正并行执行。但是,仍然有其适用的场景。毕竟,对于IO密集型的任务来说,影响是比较小的。因为IO操作会导致线程等待,GIL会释放其他线程的执行机会,从而提高程序的并发性能。

所以,Python中的多线程,通常可以在如下场景中继续使用。

1、构建响应式界面

比如我们在GUI编程中,涉及到一些耗时任务的执行,但又不能让用户界面始终卡住、无法响应用户交互。

这时,使用多进程的话,实现会相对复杂一些,但是,多线程刚好合适。

将耗时任务的线程推入后台,确保用户仍然可以操作界面。

此外,在需要执行大量、频繁的IO操作时,还是可以受益的。

2、委派工作

一个典型的实例就是,API应用对外服务执行多个HTTP请求。

在这种情况下,多线程的实现是比较合理的。当发起一个HTTP请求时,大多数时间花在从TCP套接字读取数据,这是一个典型的阻塞IO操作,因此CPython会在执行recv()的C函数时释放GIL,从而大大提高应用程序的性能。

3、多用户应用

在大部分场景下,多线程仍然应当作为多用户应用的并发基础。比如,WEB服务器等。

因为在多用户应用程序中,通过多线程启用并发要比使用多进程的代价要小很多。单独的进程需要更多的资源,因为需要为每一个进程加载新的Python解释器。

Python3.13中的实验特性

虽然GIL的存在有其合理性,但是,要求移除GIL的声音始终都没有停止,虽然,一直进展比较缓慢。

终于在Python最新版本(3.13)中,有了一些进展:

PEP-703是关于移除GIL的计划,感兴趣的同学可以查看下面的链接:https://peps.python.org/pep-0703/

从Python3.13的相关说明中也可以看到:

移除GIL的特性,在Python3.13中叫做Free-threaded模式,也就是自由线程模式,从字面意思上,也可以看出,在该模式下的线程更加自由了,不再受制于GIL。

笔者对该特性比较好奇,所以下载了最新的3.13版本进行尝试。

由于Free-threaded是一个实验性的特性,所以安装的过程中,需要特别注意,默认安装选项是不会安装该特性的。

以笔者MacOS上的安装为例,需要单击“自定”,找到Free-threaded选项,然后勾上才可以。

Python官网的Python3.13安装说明的链接放在这里,供各位参考:

Windows:https://docs.python.org/3.13/using/windows.html

MacOS:https://docs.python.org/3.13/using/mac.html#install-freethreaded-macos

安装完成后,在命令行路径中可以直接找到python3.13t,是一个链接,可以找到其绝对路径

要使用python3.13t,一种方式是直接通过命令行方式执行python脚本文件,原来的python3 变更为 python3.13t即可。

另一种方式,在IDE,比如PyCharm中,修改项目的Python解释器,更改为python3.13t所指向的绝对路径即可。

还是本文开头的代码,这次我们通过python3.13t来执行,看下效果:

从执行结果可以看出:

1、移除GIL之后,多线程确实实现了真正的并行,执行效率得到了提升。

2、单线程的执行效率受到了影响。

所以,之所以是个实验特性,而且移除GIL的尝试进行了这么久,一直没有真正实现。一个最大的顾虑就在于,移除GIL不能显著影响单线程的执行效率。

所以,很多问题不是没有解法,只是要做各种复杂的权衡。

总结

本文简单介绍了GIL的存在,通过代码演示了Python中多线程不能真正并发的现象。最后试用了Python3.13中的自由线程模式,对比了移除GIL后的多线程和单线程的执行效率。

感谢您的拨冗阅读,希望对您有所帮助。

0 阅读:14

南宫理的日志录

简介:深耕IT科技,探索技术与人文的交集