python ctypes 多线程_python 多线程与GIL_weixin_39687468的博客-程序员宅基地

技术标签: python ctypes 多线程  

GIL 与 Python 线程的纠葛

GIL 是什么?它对 python 程序会产生怎样的影响?我们先来看一个问题。运行下面这段 python 代码,CPU 占用率是多少?

#请勿在工作中模仿,危险:)

defdead_loop():whileTrue:passdead_loop()

答案是什么呢,占用 100% CPU?那是单核!还得是没有超线程的古董 CPU。在我的双核 CPU 上,这个死循环只会吃掉我一个核的工作负荷,也就是只占用 50% CPU。那如何能让它在双核机器上占用 100% 的 CPU 呢?答案很容易想到,用两个线程就行了,线程不正是并发分享 CPU 运算资源的吗。可惜答案虽然对了,但做起来可没那么简单。下面的程序在主线程之外又起了一个死循环的线程

importthreadingdefdead_loop():whileTrue:pass

#新起一个死循环线程

t = threading.Thread(target=dead_loop)

t.start()#主线程也进入死循环

dead_loop()

t.join()

按道理它应该能做到占用两个核的 CPU 资源,可是实际运行情况却是没有什么改变,还是只占了 50% CPU 不到。这又是为什么呢?难道 python 线程不是操作系统的原生线程?打开 system monitor 一探究竟,这个占了 50% 的 python 进程确实是有两个线程在跑。那这两个死循环的线程为何不能占满双核 CPU 资源呢?其实幕后的黑手就是 GIL。

GIL 的迷思:痛并快乐着

GIL 的全程为 Global Interpreter Lock ,意即全局解释器锁。在 Python 语言的主流实现 CPython 中,GIL 是一个货真价实的全局线程锁,在解释器解释执行任何 Python 代码时,都需要先获得这把锁才行,在遇到 I/O 操作时会释放这把锁。如果是纯计算的程序,没有 I/O 操作,解释器会每隔 100 次操作就释放这把锁,让别的线程有机会执行(这个次数可以通过 sys.setcheckinterval 来调整)。所以虽然 CPython 的线程库直接封装操作系统的原生线程,但 CPython 进程做为一个整体,同一时间只会有一个获得了 GIL 的线程在跑,其它的线程都处于等待状态等着 GIL 的释放。这也就解释了我们上面的实验结果:虽然有两个死循环的线程,而且有两个物理 CPU 内核,但因为 GIL 的限制,两个线程只是做着分时切换,总的 CPU 占用率还略低于 50%。

看起来 python 很不给力啊。GIL 直接导致 CPython 不能利用物理多核的性能加速运算。那为什么会有这样的设计呢?我猜想应该还是历史遗留问题。多核 CPU 在 1990 年代还属于类科幻,Guido van Rossum 在创造 python 的时候,也想不到他的语言有一天会被用到很可能 1000+ 个核的 CPU 上面,一个全局锁搞定多线程安全在那个时代应该是最简单经济的设计了。简单而又能满足需求,那就是合适的设计(对设计来说,应该只有合适与否,而没有好与不好)。怪只怪硬件的发展实在太快了,摩尔定律给软件业的红利这么快就要到头了。短短 20 年不到,代码工人就不能指望仅仅靠升级 CPU 就能让老软件跑的更快了。在多核时代,编程的免费午餐没有了。如果程序不能用并发挤干每个核的运算性能,那就意谓着会被淘汰。对软件如此,对语言也是一样。那 Python 的对策呢?

Python 的应对很简单,以不变应万变。在最新的 python 3 中依然有 GIL。之所以不去掉,原因嘛,不外以下几点:

欲练神功,挥刀自宫:

CPython 的 GIL 本意是用来保护所有全局的解释器和环境状态变量的。如果去掉 GIL,就需要多个更细粒度的锁对解释器的众多全局状态进行保护。或者采用 Lock-Free 算法。无论哪一种,要做到多线程安全都会比单使用 GIL 一个锁要难的多。而且改动的对象还是有 20 年历史的 CPython 代码树,更不论有这么多第三方的扩展也在依赖 GIL。对 Python 社区来说,这不异于挥刀自宫,重新来过。

就算自宫,也未必成功:

有位牛人曾经做了一个验证用的 CPython,将 GIL 去掉,加入了更多的细粒度锁。但是经过实际的测试,对单线程程序来说,这个版本有很大的性能下降,只有在利用的物理 CPU 超过一定数目后,才会比 GIL 版本的性能好。这也难怪。单线程本来就不需要什么锁。单就锁管理本身来说,锁 GIL 这个粗粒度的锁肯定比管理众多细粒度的锁要快的多。而现在绝大部分的 python 程序都是单线程的。再者,从需求来说,使用 python 绝不是因为看中它的运算性能。就算能利用多核,它的性能也不可能和 C/C++ 比肩。费了大力气把 GIL 拿掉,反而让大部分的程序都变慢了,这不是南辕北辙吗。

难道 Python 这么优秀的语言真的仅仅因为改动困难和意义不大就放弃多核时代了吗?其实,不做改动最最重要的原因还在于:不用自宫,也一样能成功!

其它神功

那除了切掉 GIL 外,果然还有方法让 Python 在多核时代活的滋润?让我们回到本文最初的那个问题:如何能让这个死循环的 Python 脚本在双核机器上占用 100% 的 CPU?其实最简单的答案应该是:运行两个 python 死循环的程序!也就是说,用两个分别占满一个 CPU 内核的 python 进程来做到。确实,多进程也是利用多个 CPU 的好方法。只是进程间内存地址空间独立,互相协同通信要比多线程麻烦很多。有感于此,Python 在 2.6 里新引入了 multiprocessing 这个多进程标准库,让多进程的 python 程序编写简化到类似多线程的程度,大大减轻了 GIL 带来的不能利用多核的尴尬。

这还只是一个方法,如果不想用多进程这样重量级的解决方案,还有个更彻底的方案,放弃 Python,改用 C/C++。当然,你也不用做的这么绝,只需要把关键部分用 C/C++ 写成 Python 扩展,其它部分还是用 Python 来写,让 Python 的归 Python,C 的归 C。一般计算密集性的程序都会用 C 代码编写并通过扩展的方式集成到 Python 脚本里(如 NumPy 模块)。在扩展里就完全可以用 C 创建原生线程,而且不用锁 GIL,充分利用 CPU 的计算资源了。不过,写 Python 扩展总是让人觉得很复杂。好在 Python 还有另一种与 C 模块进行互通的机制 : ctypes

利用 ctypes 绕过 GIL

ctypes 与 Python 扩展不同,它可以让 Python 直接调用任意的 C 动态库的导出函数。你所要做的只是用 ctypes 写些 python 代码即可。最酷的是,ctypes 会在调用 C 函数前释放 GIL。所以,我们可以通过 ctypes 和 C 动态库来让 python 充分利用物理内核的计算能力。让我们来实际验证一下,这次我们用 C 写一个死循环函数

extern"C"{

void DeadLoop()

{while(true);

}

}

用上面的 C 代码编译生成动态库 libdead_loop.so (Windows 上是 dead_loop.dll)

,接着就要利用 ctypes 来在 python 里 load 这个动态库,分别在主线程和新建线程里调用其中的 DeadLoop

from ctypes import *

from threading importThread

lib= cdll.LoadLibrary("libdead_loop.so")

t= Thread(target=lib.DeadLoop)

t.start()

lib.DeadLoop()

这回再看看 system monitor,Python 解释器进程有两个线程在跑,而且双核 CPU 全被占满了,ctypes 确实很给力!需要提醒的是,GIL 是被 ctypes 在调用 C 函数前释放的。但是 Python 解释器还是会在执行任意一段 Python 代码时锁 GIL 的。如果你使用 Python 的代码做为 C 函数的 callback,那么只要 Python 的 callback 方法被执行时,GIL 还是会跳出来的。比如下面的例子:

extern"C"{

typedef void Callback();

void Call(Callback*callback)

{

callback();

}

}from ctypes import *

from threading importThreaddefdead_loop():whileTrue:passlib= cdll.LoadLibrary("libcall.so")

Callback=CFUNCTYPE(None)

callback=Callback(dead_loop)

t= Thread(target=lib.Call, args=(callback,))

t.start()

lib.Call(callback)

注意这里与上个例子的不同之处,这次的死循环是发生在 Python 代码里 (DeadLoop 函数) 而 C 代码只是负责去调用这个 callback 而已。运行这个例子,你会发现 CPU 占用率还是只有 50% 不到。GIL 又起作用了。

其实,从上面的例子,我们还能看出 ctypes 的一个应用,那就是用 Python 写自动化测试用例,通过 ctypes 直接调用 C 模块的接口来对这个模块进行黑盒测试,哪怕是有关该模块 C 接口的多线程安全方面的测试,ctypes 也一样能做到。

结语

虽然 CPython 的线程库封装了操作系统的原生线程,但却因为 GIL 的存在导致多线程不能利用多个 CPU 内核的计算能力。好在现在 Python 有了易筋经(multiprocessing), 吸星大法(C 语言扩展机制)和独孤九剑(ctypes),足以应付多核时代的挑战,GIL 切还是不切已经不重要了,不是吗。

文章转自:http://zhuoqiang.me/python-thread-gil-and-ctypes.html

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_39687468/article/details/110473461

智能推荐

C语言-判断线段是否与矩形范围有交集_Triple_Vip的博客-程序员宅基地

原文地址:http://blog.csdn.net/hbuxiaoshe/article/details/5833094判断线段AB是否与矩形范围有交集这里的矩形指的是边与坐标轴平行的矩形,可用x和y上最大最小值表示。判断是否相交,先快速排斥,再做跨立,通过向量的叉积判断矩形的四个顶点是否在线段的两侧,是说明有交集。(如果判断与矩形的边是否有交集的话,可判断线段是否

论文解读——Temporal Recommendation on Graphs via Long- and Short-term Preference Fusion_百载文枢江左的博客-程序员宅基地

Temporal Recommendation on Graphs via Long- and Short-term Preference Fusion简介主要内容简介该文是项亮在KDD10会议上的论文,主要关注点在temporal recommendation上。该文为了对用户的长期偏好和短期偏好进行建模,并用于时序推荐之中,提出了一个可以对用户长期偏好和短期偏好同时建模的基于会话的时序图(...

HBase过滤器实现表复杂查询_、Packager的博客-程序员宅基地_hbase复杂查询

HBase过滤器实现表复杂查询一、引入maven依赖 <dependencies> <dependency> <groupId>org.apache.hbase</groupId> <artifactId>hbase-client</artifactId&gt...

Pointers_待用苦咖啡的博客-程序员宅基地

PointersVariables:HasA nameAn addressA sizeA value Address of: & Pointers: *Used to denote a pointerA variable which will hold the address of some other variableRemem

C++ : Online Judge 输入输出练习_麻城明歌的博客-程序员宅基地

C++ : Online Judge 输入输出练习目录:1-7、A+B(1-7)8-10、字符串排序(1-3)相关文章:OJ编程题教训相关题目:牛客网OJ在线编程常见输入输出练习场注意如何理解 while ( cin>>n ) : “读入不定多行”cin 是标准输入流对象,即istream类的对象,其从标准输入缓冲区中读出数据;而每次从键盘键入时,只有按下回车,...

1562 玻璃切割_weixin_30266829的博客-程序员宅基地

1562 玻璃切割题目来源: CodeForces基准时间限制:1.5 秒 空间限制:131072 KB 分值: 20 难度:3级算法题 收藏 关注现在有一块玻璃,是长方形的(w 毫米×h毫米),现在要对他进行切割。切割的方向有两种,横向和纵向。每一次切割之后就会有若干块玻璃被分成两块更小的玻璃。在切割之后玻璃不会被移动。现在想知道每次切割之后面积最...

随便推点

源码分析(1)---手写Spring事务框架_红烧咸鱼丶的博客-程序员宅基地

AOP技术概述Spring事务就是基于AOP的环绕通知和异常通知进行实现的Spring的事务分为两种(1)编程式事务:手动提交回滚事务等(2)声明式事务:有注解版本,有扫包版本Spring事务底层使用编程式事务进行包装的Spring的核心技术:AOP和IOCAOP就是面向切面编程,能够解决代码的复用问题AOP编程的核心点:在方法之前或者之后处理事情AOP底层的实现原理:代理设计...

PTA L1-027 出租 (20分)_m0_46368082的博客-程序员宅基地

L1-027 出租 (20分)下面是新浪微博上曾经很火的一张图:一时间网上一片求救声,急问这个怎么破。其实这段代码很简单,index数组就是arr数组的下标,index[0]=2 对应 arr[2]=1,index[1]=0 对应 arr[0]=8,index[2]=3 对应 arr[3]=0,以此类推…… 很容易得到电话号码是18013820100。本题要求你编写一个程序,为任何一个电话号码生成这段代码 —— 事实上,只要生成最前面两行就可以了,后面内容是不变的。输入格式:输入在一行中给出一个

用户登录时显示 -bash-4.2$ 问题_weixin_34295316的博客-程序员宅基地

普通用户在登录时,会出现-bash-4.2$ 的状态[[email protected] home]#su cspgs bash-4.2$ 查看 /etc/passwd 文件,显示用户 cspgs 的信息cspgs:x:1000:1000::/home/cspgs:/bin/bash查看 /home目录下没有用户 cspgs的目录,查看后是无用户cspgs的目录的原因:在...

UVA-424 - Integer Inquiry_weixin_30332241的博客-程序员宅基地

Integer InquiryOne of the first users of BIT's new supercomputer was Chip Diller. He extended his exploration of powers of 3 to go from 0 to 333 and he explored taking various...

多个微服务的接口依赖如何测试_微服务的集成测试,怎么做才高效?_weixin_39668282的博客-程序员宅基地

传统集成测试之殇传统的集成测试是将不同的单元/部件按照业务组合,对其间的协作进行正确性检验的工作。通常,集成测试发生在单元测试之后,端到端测试之前。由于集成测试阶段开始较晚,团队会花费几周甚至数月来验证各个部件之间的协作,发现问题和缺陷修复的成本很高。对于集成测试产生的收益,社区褒贬不一。J.B. Rainsberge博士在其文Integrated tests are scam中列举了集成测试的弊...

scala eclipse plugin 插件安装_weixin_30314813的博客-程序员宅基地

最近在看Apache Apollo 代码,其中有很多scala代码,没办法需要安装一个scala插件。我试过zip 安装,直接下载的update-site.zip 不能直接安装到位。我又特别懒,不想复制文件夹。所以我是通过help->install new software 安装。add 输入网址:网址的连接从这里(官网)来(不要去看人家的blog ,特别是中文blog 都过时了...