Python游戏服务器开发日记(六) 解决GIL难题!——Python再认识

        本系列文章的第一章就已经提到,用加载多个python动态链接库的方式,可以巧妙的避开GIL,实现多个python环境运行在同一个进程内。

        但是从上一个试验,到思考具体的实际用法,又经过了长达一周多的思考试验。到此时,Meme服务器框架的最底层疑难才彻底明朗。本文将彻底分析多线程下多python环境的使用。

        刚才发现,在博客里粘代码是件很无聊的事,特别是C代码,又臭又长,非常没有耐心阅读。不如在文章里只谈思想,以后有机会再把代码开源吧。


        本系列第二篇,给出了在多个线程内加载多个python so的代码,如果py脚本非常简单的话,能够做到几个线程同时执行,并行不悖。

        设计上,我们的这N个线程,实际对应skynet的N个worker,可以叫做工作线程。

        在skynet里,只有指定个数的worker线程(与CPU核心数相当),他们收到每个消息时,都会执行任务,然后返回结果。在skynet里,坚持了“干净的环境”这一概念,它为每一个service提供一个隔离的环境。有多少service,基本就有多少lua_state(也存在没有lua_state的纯C线程,一般不会太多)。特别是对应玩家的agent服务,每个玩家都对应一个agent service,这样服务器端就存在海量的lua_state。

        一个skynet节点,是几个线程对应海量lua_state,这个再强调一下,新手想着想着就容易混淆线程和lua_state的区别。另外新手最好不要把lua_state和虚拟机等价,实际上lua_state是一个静态结构,可以看成不会执行的程序,是一套静态的虚拟机。只有当worker线程挂进这个lua_state,进行处理时,这个lua_state才动了起来。

        话说回meme,meme框架受python实现的影响,不可能做成海量的python环境(python interpreter),而是仅有与线程数相当的python环境。这样,整个程序内每个Entity,不再被封装到单独的state中,不再具有好的隔离性,而是混在一团,任意的调用。


--------------------------------------------------------------------------------------------------------------------


        仿照云风在《skynet综述》一文中总结的“skynet 核心解决什么问题”,我也总结一下“Meme核心解决什么问题”——

        1、meme核心,会将符合meme标准的python程序,加载到一个meme进程中。

        2、符合meme规范的程序,是由Entity组成的(或称为对象,Object)严格“面向对象”程序。meme框架将会并行的处理不同entity的逻辑,以及分派entity之间的消息。


--------------------------------------------------------------------------------------------------------------------


        meme的Entity不具有隔离性,其最大的优点是:对任意entity的读操作,都可以是立即的,可以不通过消息传递。这可以极大的优化程序性能。

        在实际使用中,meme会提供“安全读”的机制,处理“并发读"错误的问题。

        与skynet一样,虽然总的来说程序是并发的,但是skynet的每个service,meme的每个entity,同时只能进行一个操作,这样可以避免锁的使用。而且skynet已经为这种情况提供了很好的无锁解决方案——skynet_messagequeue。


--------------------------------------------------------------------------------------------------------------------


        概念介绍完了,通过进一步测试我们的例子,会发现两个问题:

                第一,import其他C语言标准模块时(so文件形式),会造成异常。因为不同的python环境去加载同一个完全一样的so,显然会出问题。

                第二,整个程序内,有很多Python环境,但是同一个Entity仅有一个,这些entity应该存储在哪里?


        通过对C Python的研究,我发现Python对于PyObject的管理,还是比较简单松散的。无论一个PyObject是在哪里创建的,只要它在进程内存在,而且有它的指针,那么任意一个Python虚拟机都能正缺的处理它。这个PyObj的引用计数也在PyObj内部保存。

        举个例子,两个python环境,我在其中一个python环境创建一个list, l = [1,2,3],将l的指针告诉另一个python环境,另一个python环境执行l.pop(0),修改了l。之后在这两个环境中,可以看到l都被正确的pop了(实际上确实只有一个l)。只要注意,两个环境不能同时对l做处理,同时执行会造成显然的错乱,一般结果是段错误。

        而且,不仅数字、string、list、dict是PyObject,class、module、function也是object。这说明什么呢?


        我一开始以为,meme的多个线程,应该加载相同的py代码,然后由一个线程创建对象,其他线程对这个对象进行操作。但是试验的效果让我苦恼了半天,最后发现,其实代码里的class、function,实际上也是独立的PyObj。

        简单来说,如果你写一个python脚本,里面定义了一个class A,和一个function hello(),那么在多个Python环境里import这个脚本,会产生多个A和hello。但是我搞错的一点是——这些A和hello,是多个完全不同的A,和多个完全不同的hello。我是把class A的一个对象a,发送给多个Python环境,执行

                isinstance( a, A )

        才发现原来只有对创建a的那个对象来说,a是A的亲儿子,其他环境里的A,和a完全没有关系,上面的表达式返回False。

        

        经过思考,这导向了一个非常有趣的方向——对class、function、module的定义,都只需要单独定义一次,然后把生成的这些 classObj、moduleObj 的指针,送给其他Python环境,大家就都拥有了同样的定义。

        实际操作上,我们指定一个特殊的线程,index为0,由它负责加载python代码。然后把指针全都共享出去。这也解决了上面提到的第一个问题——加载多个so库会崩溃的问题。加载多个so库也是一样,实际只加载一个so库即可。(这个地方也有疑问,因为python的某些标准库,是有状态的,多线程操作它们,可能会造成崩溃。在有必要时,还是要通过特殊手段加载多个才行。)


-----------------------------------------------------------------------------------------------------------

        meme框架,构思到这个程度,经历了很长时间。但是思路从模糊到清晰,只经历了一个下午。把思路整理出来,也不过区区几百字而已。

        很多问题的结果十分难得,而一旦点破,又觉得“就那么回事”吧。

        我想,实现它估计也要更长的时间。继续努力。


你可能感兴趣的:(Python游戏服务器开发日记(六) 解决GIL难题!——Python再认识)