当前位置: 首页 > 科技观察

Python最头疼的问题

时间:2023-03-12 10:23:28 科技观察

Python使用了全局解释锁(GIL),代码不能同时在多核上并发运行,也就是说Python的多线程不能并发,很多人使用后会发现多线程改进自己的Python代码,程序的运行效率有所下降。这篇文章介绍了Python中的全局解释器锁(GIL)。笔者认为这是Python最头疼的问题。十多年来,Python的全局解释器锁(GIL)在新手和专家中引起了很大的挫败感和好奇心。未解决的问题每个领域都有一个问题:它是如此困难,如此耗时,以至于仅仅试图解决它都会令人震惊。整个社区很久以前就放弃了这个问题,现在只有少数人在努力修复它。对于一个初学者来说,解决这么难的问题,已经足够让他名声大噪了。计算机科学中的P=NP就是这样一个问题。如果这个问题能以多项式时间复杂度解决,它真的可以改变世界。Python中最难的问题比P=NP稍微容易一些,虽然仍然没有令人满意的答案,但解决这个问题与解决P=NP一样具有革命性。正因如此,Python社区中很多人都关注了这个问题:“我可以用全局解释器锁(GIL)做什么?”要理解Python底层GIL的含义,我们需要从Python的基础开始。像C++这样的语言都是编译型语言。顾名思义,这类语言的代码输入到编译器,由编译器根据语言的语法进行解析,生成与语言无关的中间表示,最后链接成一个高度优化的机器。由代码组成的可执行程序。因为编译器可以获得整个代码(或者说是一大段相对独立的代码),所以编译器可以对代码进行深度优化。这允许它推理不同语言结构之间的交互,从而导致更有效的优化。相反,Python是一种解释型语言。代码被送入解释器运行。解释器在执行之前对代码一无所知;它只知道Python的规则,以及这些规则在执行过程中是如何动态应用的。它也有一些优化,但与编译语言的优化完全不同。由于解释器不能很好地推理代码,Python中的大部分优化实际上都是对解释器本身的优化。更快的解释器自然意味着更快的程序,而且这种优化对开发者是免费的。也就是说,解释器优化后,开发者无需修改Python代码,就可以享受到优化带来的好处。这是很重要的一点,有必要在这里强调一下。在所有条件都相同的情况下,Python程序运行的速度与解释器的“速度”直接相关。无论开发者如何优化他们的代码,程序的执行速度仍然受到解释器执行效率的限制。显然,这就是为优化Python解释器做了大量工作的原因。这几乎是Python开发人员可以获得的免费午餐。免费午餐结束了吗?摩尔定律告诉我们硬件加速的时间表,同时,整整一代的程序员都学会了如何在摩尔定律下编写代码。如果程序员编写的代码较慢,通常最简单的方法是稍等片刻,等待更快的处理器问世。事实上,摩尔定律仍然有效,并将长期有效,但它的工作方式已经发生了根本性的变化。不是将时钟频率稳定地增加到无法达到的速度,而是使用多个内核来利用增加的晶体管密度。为了让程序充分利用新处理器的性能,必须以并发方式重写代码。当大多数开发人员听到“并发”时,他们通常会立即想到多线程程序。目前,多线程仍然是利用多核系统的最常见方式。多线程编程比传统的“顺序”编程困难得多,但细心的程序员可以在他们的代码中充分利用多线程并发。由于几乎所有广泛使用的现代编程语言都支持多线程编程,因此语言对多线程的实现应该是事后诸葛亮。意想不到的事实现在我们来到了问题的关键。要利用多核系统,Python必须支持多线程。作为一种解释型语言,Python的解释器对多线程的支持必须是既安全又高效的。我们都知道多线程编程带来的问题。解释器必须避免不同线程操作内部共享数据。同时要保证用户线程能够完成尽可能多的计算。那么当不同线程同时访问数据时,如何保护数据呢?答案是全局解释器锁。顾名思义,这是解释器上的全局锁(在互斥锁或类似的意义上)。这种方式是非常安全的,但是(对于Python初学者来说)这也意味着:对于任何一个Python程序,无论有多少个线程或处理器,任何时候都只有一个线程在执行。许多人无意中发现了这个事实。在线讨论组和留言板上充斥着来自Python新手和专家的问题:为什么我全新的多线程Python程序运行速度比只有一个线程时慢?在问这个问题的时候,很多人也觉得自己像个傻子,因为如果程序确实是可并行化的,那么双线程的程序显然比单线程的要快。事实上,这个问题已经被问了很多次,以至于Python专家已经准备好了一个规范的答案:不要使用多线程,使用多处理。但答案比问题本身更令人困惑:我不能在Python中使用多线程吗?在像Python这样流行的语言中使用多线程有多糟糕,甚至专家都反对它。有什么我不明白的吗?很不幸的是,不行。由于Python解释器的设计,使用多线程来提高性能可能是一项艰巨的任务。在最坏的情况下,多线程实际上会减慢(有时会显着)您的程序。一个计算机科学专业的新生可以告诉你当多个线程竞争共享资源时会发生什么。结果通常并不理想。多线程在很多情况下工作得很好,对于解释器实现和内核开发者来说,不要过多抱怨Python多线程性能可能是他们最大的愿望。我们现在应该做什么?恐慌?我们现在能做什么?作为Python开发者,我们是否应该放弃使用多线程来实现并行?为什么GIL一次只允许一个线程运行?难道不能在访问多个独立对象时使用更细粒度的锁来保护它们吗?为什么没有人尝试过类似的东西?这些都是有用的问题,他们的答案也很有趣。GIL为访问许多对象提供这种保护,例如当前线程状态和用于垃圾收集的堆分配对象。这对于需要使用GIL的Python语言来说并不奇怪。这是该实现的产物。现在还有不使用GIL的Python解释器(和编译器)。但是对于CPython,GIL从一开始就存在。那么我们为什么不放弃GIL呢?很多人可能不知道,1999年,GregStein为Python1.5提交了一个名为“freethreading”的补丁。这个补丁经常被提及但不是很了解。这个补丁试图完全删除GIL并用细粒度锁替换它。但是去除GIL的代价是单线程程序执行速度的下降,可以降低40%左右。使用两个线程提供了一些加速,但加速并没有随着核心数量线性增长。由于执行速度慢,这个补丁没有被接受,几乎被遗忘了。GIL很头疼,还是想点别的吧虽然“freethreading”补丁没被采纳,但还是有启发的。它展示了Python解释器的一个基本点:移除GIL非常困难。解释器现在比发布此补丁时依赖更多的全局状态,这使得删除GIL变得更加困难。值得一提的是,正是出于这个原因,许多人对删除GIL变得更感兴趣。难题通常很有趣。但这可能有点误导。让我们假设:如果我们有一个移除GIL并且不会降低单线程Python代码性能的神奇补丁,我们就会得到我们一直想要的东西:一个同时使用所有处理器的线程API。现在我们得到了我们所希望的,但这真的是一件好事吗?基于线程的编程很难。当一个人自以为对线程了如指掌时,总会出现一些新的问题。一些非常知名的语言设计者和研究者都站出来反对线程模型,因为在这方面要获得合理的一致性实在是太难了。编写过多线程应用程序的任何人都可以告诉您,开发和调试比单线程应用程序难得多。程序员的心智模型往往适合顺序执行模型,恰恰不适合并行执行模型。GIL的存在无意中让开发人员免于陷入困境。在使用多线程的时候还是需要同步原语的,GIL其实是帮助我们保证了不同线程之间数据的一致性。所以Python中最难的问题似乎是问错了一点。Python专家推荐使用多处理而不是多线程是有道理的,而不是试图羞辱Python线程。Python的这种实现方式鼓励开发人员以更安全、更直观的方式实现并发模型,同时保留使用多线程进行开发,让开发人员在需要的时候使用。大多数人可能不知道最好的并行编程模型是什么。但是大多数人都知道多线程方法并不是最好的并行模型。不要认为GIL是一成不变的或不合理的。AntoinePitrou在Python3.2中实现了一个新的GIL,显着改进了Python解释器。这是自1992年以来对GIL最重大的改进。这种变化是如此剧烈,以至于在这里很难解释,但在较高的层次上,旧的GIL统计了Python指令来确定何时发布GIL。由于Python指令和翻译后的机器指令之间没有一对一的对应关系,因此一条Python指令可能需要大量工作。新的GIL使用固定的超时时间来指示当前线程释放锁。当当前线程持有锁,第二个线程申请锁时,当前线程会在5ms后强制释放锁(即当前线程每5ms检查一次是否需要释放锁)。这使得在可以执行任务的情况下更容易预测线程之间的切换。然而,这并不是绝对的改进。DavidBeazley可能是研究GIL在执行不同类型任务中的作用的最活跃的研究人员。除了对Python3.2之前的GIL研究最深入之外,他还查看了最新的GIL实现,发现了很多有趣的程序场景,即使是新的GIL实现也表现得相当糟糕。他仍在通过实践研究和发表成果来推动关于GIL的讨论。不管人们如何看待Python的GIL,它仍然是Python语言中最困难的技术挑战。理解它的实现需要对操作系统设计、多线程编程、C语言、解释器设计和CPython解释器的实现有非常透彻的理解。仅这些前提就阻止了许多开发人员更透彻地研究GIL。但是,没有迹象表明GIL会很快消失。目前,它继续让那些Python新手和对解决技术问题感兴趣的人感到困惑和惊讶。以上是基于我目前对Python解释器的研究。我打算写解释器的其他方面,但没有比GIL更广为人知的了。虽然这些技术细节来自我对CPython代码库的深入研究,但仍可能存在不准确之处。如果您发现内容不准确,请及时告诉我,我会尽快更正。