社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

JVM和Python解释器的硬盘夜话

码农翻身 • 8 年前 • 827 次点击  

这个电脑的主人是个程序员,他相继学习了C, Java ,Python, Go, 但是似乎停留在Hello World的水平。 


随着hello.c, HelloWorld.java , Hello.py等文件被删除,曾经热闹非凡的硬盘夜话也冷清了起来.....


(码农翻身友情提示:参见文末的硬盘夜话系列文章)


JVM先生


JVM先生发觉有点不太对劲,原来那些围着自己献殷勤的Java文件都不见了。


茫然四顾,也找不到一个可以执行的class文件, JVM先生觉得非常孤独。


到隔壁目录逛逛吧,说不定还有点新发现。 


果然,隔壁目录是正在发呆的Python解释器,JVM先生曾经见主人用它执行过一次Hello.py。


当Python明白JVM先生的处境,不由得幸灾乐祸起来: “看来你活不久了,传说中可怕的卸载很快就会来找你了。”


“怎么可能?你才活不久! 可能你还不知道吧,Hello.py也去回收站享清福了,你现在和我一样,都是孤家寡人!” JVM先生马上反驳, “再说了,主人怎么可能卸载我? Java可是世界上使用者最多的语言。”


“你没看到主人穿的T恤上写的字吗? 人生苦短,我用Python,这已经充分说明一切了。” Python解释器补了一刀。 


“得意什么? 你不就是个小小的解释器吗? 怎么能和我这性能卓越的虚拟机相比?”


“解释器? 你居然当我是解释器? 我明明是虚拟机好不好?别以为只有你有字节码,我也有。”  Python解释器急忙澄清自己的身份。 


“那你还不是解释执行的?” JVM先生有点底气不足。


“你是只知其一,不知其二,我看起来是直接解释执行的,实际上我在背后把Python文件做了编译,也形成了字节码。”


说着,Python给出了一段自己的字节码


LOAD_FAST       0 (x)
LOAD_FAST       1 (y)
BINARY_ADD
LOAD_CONST      1 (10)
BINARY_MULTIPLY
RETURN_VALUE


经验老道的JVM先生一眼就看出来,这是基于栈的虚拟机!


你看它先把x, y 两个变量从某个地方给取出来,压入栈中, 然后弹出,做加法运算,把结果也压入栈中。 


接下来把常量10 压入栈中,把上个结果(x+y) 和10 进行相乘, 最后返回。


其实这段代码表达的就是 (x+y)*10 ! 和自己的JVM字节码真是非常像!


(码农翻身友情提示: 在《我是一个Java Class》中对基于栈的操作有漫画描述)


虽然胸有激雷, 但JVM压抑着努力做到面如平湖, 他淡淡地说:这不就是 (x+y)*10 嘛!



垃圾回收


“哈哈,我就知道老兄你一眼就能看透, 除此之外,我也有垃圾回收呢,主人只需要把对象创建起来,根本不用管什么时候把对象占据的空间和释放掉。” Python再次抛出炸弹。


“垃圾回收?你是怎么做垃圾回收的? ” JVM先生一下子兴奋起来,这可是他最厉害的领域之一,Python竟然敢班门弄斧!


“我主要使用简单明了的引用计数法。” Python很得意。


所谓引用计数法就是给每个对象都增加一个“引用计数”的字段,每次有新的变量指向了对象A,A的引用计数就会加一,变量指向了别的对象,A的引用计数就是减一,当引用计数为0 ,就意味着对象A可以被回收了。 


1a1 = ClassA()   # a1指向对象(简称对象A)的引用计数为 1
2a2 = a1         # a1,a2 指向同一个对象,对象A引用计数为 2
3a1 = ClassB()   # a1 指向新的对象, 对象A的引用计数变为1

(可左右滑动)


“看起来简单,实际上一点都不简单,每次遇到变量的赋值操作的时候,你都得把增加新对象的引用计数,还得减少老对象的引用计数,更要命的是循环引用问题, 你怎么解决?”  JVM先生问道。


1a = ClassA()   # 对象A的引用计数为1
2b = ClassB()   # 对象B的引用计数为1
3a.t = b        # 对象B的引用计数为2
4b.t = a        # 对象A的引用计数为2
5del a          # 对象A还在被b所引用,引用计数还是为1,无法删除
6del b          # 对象B还在被a所引用,引用计数还是为1,无法删除

(可左右滑动)


Python嘿嘿一笑:“我不是说了吗,我主要是引用计数,我还有标记-清除,分代回收等算法作为辅助呢,从一个根集合开始,查找还被引用的,需要存活的对象...... 想来你是十分熟悉了。”


JVM先生当然很熟悉,想想自己的年轻代(里边还要划分成eden,survivor),年老代,Minor GC,Full GC,各种各样的垃圾收集器Serial、PraNew、Parallel Scavenge,Serial Old、Parallel Old、CMS,各种各样的参数调优,经常把新手搞得眼花缭乱,又兴奋又迷茫。 


没想到这小子也有一套标记-清除,分代回收,看来在理论基础上就难于压倒他了。 


“可是,网上讨论Java 垃圾回收的文章铺天盖地,为什么很少人讨论Python垃圾回收的参数,调优啊?是不是你做得不怎么样啊?”  JVM先生很疑惑。


“嘿嘿,那是因为我就不给Python程序员提供那些烦人的调优选项,你只要用就行了,难道你写个Python脚本还要关注垃圾回收吗? 没必要! 人生苦短,我用Python,很有道理!”


GIL


“既然你用引用计数,怎么处理多个线程同时修改一个对象的引用计数问题? 如果引用计数被错误地修改, 很可能会导致一个对象一直不被回收,或者回收了一个不能被回收对象。 难道你在每个对象上都加了一把锁? 只让一个线程进入修改?”  JVM 的思考颇有深度。


“嘿嘿,我没有在每个对象上都加锁,每次访问都加锁、解锁,开销太大! 并且还很容易引发死锁相反, 我只设置了一把锁,Global Interpreter Lock ,简称GIL, 这把超级大锁只允许一个线程获得Python解释器的控制权, 简单来说,同一时刻,只有一个线程能运行!”


“同一时刻,只有一个线程能运行? ” JVM简直不敢相信,这绝对颠覆了自己的世界观和人生观。 


用户写了多线程的程序,如果CPU有多核,只有一个线程执行,怎么利用多核?  是为了实现“一核有难,多核围观”吗? 


线程切换的时候还得释放GIL,竞争GIL,多线程可能跑得比单线程都慢了! 要多线程有什么用? 


“其实也没什么大不了的,老兄你也知道,这程序的瓶颈啊,它不在CPU, 而在于IO, 就是用户的输入,数据库的查许,网络的访问, 线程等到有IO操作的时候,放弃GIL这个超级大锁,让别的线程去执行就是了。”



“那要是有个CPU密集型的线程在执行,根本没有I/O, 一直霸占着GIL不放,那该怎么办? ”  JVM先生问道。


“放心吧,我肯定不能让他霸占着CPU不放,我也得给别的线程一个机会运行。 具体的做法也很简单,每当线程执行了100 ticks, 就需要释放这个GIL。”


“tick ? 是时钟周期吗?”  


“不是时钟周期,是和我的字节码相关的,一个tick映射到一条或多条字节码。”


“当线程A执行了100个ticks以后,你就让他放弃GIL,然后具体怎么处理?”  JVM先生刨根问底。 


“然后我就发个信号给操作系统老大喽,让他去调度那些因为没有获得GIL锁而挂起的线程,大家去竞争这把锁,当然线程A也会参与竞争,大家都站在同一个起跑线上,谁获得了GIL, 谁就可以执行了。 ”



JVM觉得Python的这种作法实在是古怪,操作系统老大本来有一套自己的线程调度的策略,现在你为了让线程释放GIL, 又来搞个什么ticks,  把简单的东西给变复杂了啊。 


JVM先生很快想到另外一个问题: “线程A也会参与竞争?! 那要是在多核情况下,被分配到其他核的线程由于需要等待信号,唤醒以后才能竞争,线程A会不会经常抢先,‘打压’别的线程,让它们难以抬头,难以运行? ”



Python不由得佩服JVM,它在这方面知识储备真厉害,一下子就抓住了关键的小尾巴。他尴尬地笑了笑: “嗯,有这个可能。 ”


JVM从打心底鄙视这种GIL的全局锁,太不讲人性了。 


“如果真想利用多核的特性,还想避开GIL, Python专家建议,还是用多进程吧! ”  Python无奈地说道。 


“多进程? 你要知道每个进程都是独立的,数据共享起来比线程要麻烦得多! 程序不经过大改动是不行的。 你们怎么不把这个不讲人性的GIL给去掉啊??”


“哎呀,不好改啊,历史遗留问题了, 我们Python诞生于上世纪90年代初,比你Java 还早。 Python的设计目标就是易于使用,易于扩展,很多用C语言写的扩展库被开发出来,由于有GIL, 这些扩展库都不必考虑线程安全问题,很容易被集成进来。”


看来存在就是合理的,C的扩展库极大地丰富了Python的功能,促进了Python的发展和使用。


但是随之多核的出现和流行,GIL慢慢地不合时宜了。关键是现在想去修改也很难了。 


“那你们有没有计划,什么时候把GIL给干掉?”


“我觉得等到Python 3000也许有戏。”  Python开玩笑,他还挺乐观。 


JVM先生突然想到一件事情:“我听说你们Python语言在我的JVM上也有实现,叫做什么Jython,它有GIL的限制吗?”


“Jython啊,他在底层都被编译成你的Java字节码了,在你的虚拟中运行,是没有GIL的。”


“哼哼,还是我的平台厉害吧!” JVM先生很得意。


尾声


两人正聊得热火朝天, 突然看到主人回到电脑前,拿起鼠标,敲起键盘,不知道要做什么事情。


两人非常紧张,惴惴不安地迎接最终的审判: 卸载。 


可怕的卸载并没有来临, 相反,电脑里入住了两个IDE, 一个是IntelliJ IDEA, 还有一个是PyCharm,两人不由得欢呼起来: 看来主人并不打算抛弃我们,而是要用IDE做点大项目了!


相关阅读:

C老头和Java小子的硬盘夜话

Python和Java的硬盘夜话

90后“老头儿”和00后Go小子的硬盘夜话


参考资料:

https://www.youtube.com/watch?v=ph374fJqFPE

https://realpython.com/python-gil/  


(完)

你看到的只是冰山一角, 更多精彩文章,请移步《2016文章精华》或者《2017文章精华


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/WGc9hm5DbK
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/21330