2003年初,英特尔推出了新的Pentium 4“ HT”处理器。 该处理器的主频为3 GHz,并具有“超线程”技术。
在接下来的几年中,英特尔和AMD一直在通过提高总线速度,L2缓存大小和减小裸片大小以最大程度地减少延迟来争取最佳台式计算机性能。 3Ghz HT于2004年被时钟频率高达4 GHz的“ Prescott”型号580取代。
更高的时钟频率似乎是提高性能的前进方向,但是CPU受到高功耗和温暖地球的热量输出的困扰。
台式机中是否有4Ghz CPU? 不太可能,因为提高性能的方法是更高的总线速度和多核。 英特尔酷睿2在2006年取代了奔腾4,时钟速度大大降低。
除了发布消费型多核CPU之外,2006年还发生了其他事情,Python 2.5也发布了! Python 2.5附带了您熟悉和喜欢的with语句的beta版本。
当使用Intel的Core 2或AMD的Athlon X2时,Python 2.5有一个主要限制。
GIL。
GIL或全局解释器锁是Python解释器中的一个布尔值,受互斥锁保护。 CPython中的核心字节码评估循环使用该锁来设置当前正在执行语句的线程。
CPython在单个解释器中支持多个线程,但是线程必须请求访问GIL才能执行操作码(低级操作)。 反过来,这意味着Python开发人员可以利用异步代码,多线程代码,而不必担心获取任何变量的锁或使进程因死锁而崩溃。
GIL使使用Python进行多线程编程变得简单。
GIL还意味着,尽管CPython可以是多线程的,但在任何给定时间只能执行1个线程。 这意味着您的四核CPU正在执行此操作-(希望减去蓝屏)
当前版本的GIL 写于2009年 ,以支持异步功能,即使在多次尝试删除或降低对GIL的要求后,它仍未受影响。
任何删除GIL的建议的要求是,它不应降低任何单线程代码的性能。 早在2003年启用超线程的任何人都会欣赏为什么这很重要 。
如果要在CPython中使用真正的并发代码,则必须使用多个进程。
在CPython 2.6中, 多处理模块已添加到标准库中。 多重处理是CPython进程(每个都有自己的GIL)的产生的包装器-
from multiprocessing import Process def f(name): print 'hello', name if __name__ == '__main__': p = Process(target=f, args=('bob',)) p.start() p.join()
可以生成进程,通过已编译的Python模块或函数发送命令,然后重新加入主进程。
多重处理还支持通过队列或管道共享变量。 它还具有一个Lock对象,用于锁定主进程中的对象以从其他进程进行写入。
多重处理有1个主要缺陷。 它在时间和内存使用上都有大量开销。 即使没有无站点,CPython的启动时间也是100-200ms(请参阅https://hackernoon.com/which-is-the-fastest-version-of-python-2ae7c61a6b2b )。
因此,您可以在CPython中使用并发代码,但必须仔细计划长时间运行的进程的应用程序 ,这些进程之间几乎没有对象共享。
另一种选择是第三方程序包,如Twisted。
综上所述,CPython中的多线程很容易,但是它并不是真正的并发,多处理是并发的,但是开销却很大。
如果有更好的方法怎么办?
绕过GIL的线索就是名字,全局解释器锁是全局解释器状态的一部分。 CPython进程可以具有多个解释器,因此可以具有多个锁, 但是 ,此功能很少使用,因为它仅通过C-API公开。
为CPython 3.8提议的功能之一是PEP554,子解释器的实现和标准库中带有新解释器模块的API。
这样一来,就可以从Python创建多个解释器。 Python 3.8的另一个变化是,解释器都将具有单独的GIL-
由于解释器状态包含内存分配区域,所有指向Python对象的指针的集合(本地和全局),因此PEP 554中的子解释器无法访问其他解释器的全局变量。
与多处理类似,在解释器之间共享对象的方法是序列化它们并使用一种IPC形式(网络,磁盘或共享内存)。 有许多方法可以在Python中序列化对象,包括marshal模块,pickle模块以及更标准化的方法,例如json和simplexml。 这些每个都有优点和缺点, 它们都有开销。
一等奖将是拥有一个共享的存储空间,该存储空间是可变的,并由拥有过程控制。 这样,对象可以从主解释器发送并由其他解释器接收。 这将是每个解释器都可以访问的PyObject指针的查找管理内存空间,主要过程控制锁。
API仍在制定中,但可能看起来像这样:
此示例使用numpy并通过使用marshal模块对其进行序列化在通道上发送numpy数组,然后子解释器处理数据(在单独的GIL上),因此这可能是CPU绑定的并发问题,非常适合子解释器。
封送模块相当快,但不如直接从内存中共享对象快。
PEP 574提出了一个新的pickle协议(v5),该协议支持允许将内存缓冲区与剩余的pickle流分开处理。 对于大型数据对象,一次性将它们全部序列化并从子解释器反序列化将增加很多开销。
新的API可以这样连接( 假设尚未合并)
好的,所以此示例使用低级子解释器API。 如果您使用了多处理库,您将认识到一些问题。 它不像线程那样简单,您不能仅仅说在单独的解释器中使用此输入列表运行此函数。
合并此PEP后,我希望我们会看到PyPi中的其他一些API也采用了它们。
简短答案 :多于一个线程,少于一个进程。
长答案 :解释器具有其自己的状态,因此尽管PEP554将使创建子解释器变得容易,但它需要克隆和初始化以下内容:
可以从内存中轻松克隆核心配置,但是导入的模块并不是那么简单。 在Python中导入模块的速度很慢,因此,如果创建子解释器意味着每次都将模块导入另一个名称空间,则收益会减少。
标准库中asyncio事件循环的现有实现创建了要评估的帧,但在主解释器中共享状态(因此共享GIL)。
PEP554已经经过合并,可能在Python 3.9,备选事件循环的实现,可以实现(尽管没有人那么做),其运行的异步子口译中的方法,因此,兼任。
好吧,不完全是。
由于CPython已经使用单个解释器实现了很长时间,因此代码库的许多部分使用“运行时状态”而不是“解释器状态”,因此,如果要以当前形式合并PEP554,仍然会有很多问题。
例如,垃圾收集器(在3.7 <中)状态属于运行时。
在PyCon冲刺期间, 更改已开始将垃圾收集器状态移至解释器,以便每个子解释器将拥有自己的GC(应有)。
另一个问题是CPython代码库和许多C扩展中存在一些“全局”变量。 因此,当人们突然开始编写正确的并发代码时,我们可能会开始看到一些问题。
另一个问题是文件句柄属于该过程,因此,如果您打开了要在一个解释器中写入的文件,则子解释器将无法访问该文件(无需对CPython进行进一步更改)。
简而言之,还有许多其他事情需要解决。
对于单线程应用程序,GIL将仍然存在。 因此,即使在合并PEP554时,如果您具有单线程代码,也不会突然并发执行。
如果您想要Python 3.8中的并发代码,则遇到CPU约束的并发问题,那么这可能就是问题所在!
Pickle v5和用于多处理的共享内存可能是Python 3.8(2019年10月),子解释器将在3.8和3.9之间。
如果您现在想玩我的示例,我已经构建了一个自定义分支,其中包含所有必需的代码https://github.com/tonybaloney/cpython/tree/subinterpreters
From: https://hackernoon.com/has-the-python-gil-been-slain-9440d28fa93d