加载中

For more than a decade, no single issue has caused more frustration or curiosity for Python novices and experts alike than the Global Interpreter Lock.

An Open Question

Every field has one. A problem that has been written off as too difficult, too time consuming. Merely mentioning an attempt to solve it raises eyebrows. Long after the community at large has moved on, it is taken up by those on the fringe. Attempts by novices are undertaken for no other reason than the difficulty of the problem and the imagined accolades that would accompany a solution. The open question in Computer Science of whether P = NP is such a problem. An answer to the affirmative has the possibility to literally change the world, provided a "reasonable" polynomial time algorithm is presented. Python's hardest problem is less difficult than crafting a proof of P = NP, to be sure. Nevertheless, it has not received a satisfactory solution to date, and the practical implications of a solution would be similarly transformative. Thus, it's easy to see why so many in the Python community are interested in an answer to the question: "What can be done about the Global Interpreter Lock?"

超过十年以上,没有比解释器全局锁(GIL)让Python新手和专家更有挫折感或者更有好奇心。

未解决的问题

随处都是问题。难度大、耗时多肯定是其中一个问题。仅仅是尝试解决这个问题就会让人惊讶。之前是整个社区的尝试,但现在只是外围的开发人员在努力。对于新手,去尝试解决这样的问题,主要是因为问题难度足够大,解决之后可以获得相当的荣誉。计算机科学中未解决的 P = NP 就是这样的问题。对此如果能给出多项式时间复杂度的答案,那简直就可以改变世界了。Python最困难的问题比证明P = NP要容易一些,不过迄今仍然没有一个满意的解决,要知道,这个问题的实用的解决方案同样能起着变革性的作用。正因为如此,很容易看到Python社区会有如此多的人关注于这样的问题: "对于解释器全局锁能做什么?"

A Low Level Look at Python

To understand the GIL and its implications, we must start at Python's foundations. Languages like C++ are compiled languages, so named because a program is fed in to a compiler where it is parsed according to the language's grammar, transformed into a language agnostic intermediate representation, and linked into an executable comprised of highly optimized machine code. The compiler is able to optimize the code so aggressively because it is seeing the whole program (or large, self-contained chunks) at once. This allows it to reason about interactions between different language constructs and make informed decisions about optimization.

In contrast, Python is an interpreted language. The program is fed into an interpreter in order to be run. The interpreter has no knowledge of the program before it is run; rather, it knows the rules of Python and is capable of dynamically applying those rules. It too has optimizations, but optimizations of a rather different class. Since the interpreter cannot reason about the program proper, most of Python's optimizations are optimizations of the interpreter itself. A faster interpreter means faster program execution "for free". That is, when the interpreter is optimized, Python programs need not change to realize the benefit.

This is an important point, so it bears repeating. The execution speed of a Python program, all other things being equal, is directly tied to the "speed" of the interpreter. No matter how much optimization you do within your program itself, your program's execution speed is still tied to how efficiently the interpreter can execute your code. It is clear, then, why much work has been devoted to optimizing the Python interpreter. It is the closest thing to a free lunch Python programmers can get.

Python的底层

要理解GIL的含义,我们需要从Python的基础讲起。像C++这样的语言是编译型语言,所谓编译型语言,是指程序输入到编译器,编译器再根据语言的语法进行解析,然后翻译成语言独立的中间表示,最终链接成具有高度优化的机器码的可执行程序。编译器之所以可以深层次的对代码进行优化,是因为它可以看到整个程序(或者一大块独立的部分)。这使得它可以对不同的语言指令之间的交互进行推理,从而给出更有效的优化手段。

与此相反,Python是解释型语言。程序被输入到解释器来运行。解释器在程序执行之前对其并不了解;它所知道的只是Python的规则,以及在执行过程中怎样去动态的应用这些规则。它也有一些优化,但是这基本上只是另一个级别的优化。由于解释器没法很好的对程序进行推导,Python的大部分优化其实是解释器自身的优化。更快的解释器自然意味着程序的运行也能“免费”的更快。也就是说,解释器优化后,Python程序不用做修改就可以享受优化后的好处。

这一点很重要,让我们再强调一下。如果其他条件不变,Python程序的执行速度直接与解释器的“速度”相关。不管你怎样优化自己的程序,你的程序的执行速度还是依赖于解释器执行你的程序的效率。这就很明显的解释了为什么我们需要对优化Python解释器做这么多的工作了。对于Python程序员来说,这恐怕是与免费午餐最接近的了。

The Free Lunch Is Over

Or is it? A generation of programmers have learned to write code while Moore's Law was delivering hardware based speedups with predictable timing. If one wrote code that was slow, simply waiting a bit for faster processors was oftentimes the easiest solution. Indeed, Moore's law still holds true and likely will for quite a bit longer, but the way in which it holds has fundamentally changed. No longer are clock rates steadily increasing to dizzying speeds. Instead, multiple cores are used to take advantage of transistor density increases. Programs wishing to capitalize on new processors must be rewritten to exploit parallelism.

When most developers hear "parallelism" the immediately think of multi-threaded programs. Utilizing multiple threads of execution is by far the most common way to take advantage of multi-core systems. While multi-threaded programming is a good deal tougher than "sequential" programming, the careful programmer may nevertheless exploit parallelizable portions of his or her code to great effect. The implementation language should be an afterthought, since almost all heavily used modern languages support multi-threaded programming.

免费午餐结束了

还是没有结束?摩尔定律给出了硬件速度会按照确定的时间周期增长,与此同时,整整一代程序员学会了如何编码。如果一个人写了比较慢的代码,最简单的结果通常是更快的处理器去等待代码的执行。显然,摩尔定律仍然是正确的,并且还会在很长一段时间生效,不过它提及的方式有了根本的变化。并非是时钟频率增长到一个高不可攀的速度,而是通过多核来利用晶体管密度提高带来的好处。在新处理器上运行的程序要想充分利用其性能,必须按照并发方式进行重写。

大部分开发者听到“并发”通常会立刻想到多线程的程序。目前来说,多线程执行还是利用多核系统最常用的方式。尽管多线程编程大大好于“顺序”编程,不过即便是仔细的程序员也没法在代码中将并发性做到最好。编程语言在这方面应该做的更好,大部分应用广泛的现代编程语言都会支持多线程编程。

A Surprising Fact

Now we come to the crux of the issue. To take advantage of multi-core systems, Python must support multiple threads of execution. Being an interpreted language, Python's interpreter must be written in such a way so that doing so is both safe and performant. We all know the issues that multi-threaded programming can present. The interpreter must be mindful not to operate on internally shared data from different threads. It must also manage user's threads in such a way that the maximum amount of computation is being performed at all times.

What, then, is the mechanism by which data is protected from simultaneous access by different threads? The Global Interpreter Lock. The name is instructive. Quite literally, it is a global (in the sense of the interpreter) lock (in the sense of a mutex or similar construct) on the interpreter. This approach is certainly safe, but it has (for the new Python programmer), a startling implication: in any Python program, no matter how many threads and how many processors are present, only one thread is being executed at any time.

意外的事实

现在我们来看一下问题的症结所在。要想利用多核系统,Python必须支持多线程运行。作为解释型语言,Python的解释器必须做到既安全又高效。我们都知道多线程编程会遇到的问题。解释器要留意的是避免在不同的线程操作内部共享的数据。同时它还要保证在管理用户线程时保证总是有最大化的计算资源。

那么,不同线程同时访问时,数据的保护机制是怎样的呢?答案是解释器全局锁。从名字上看能告诉我们很多东西,很显然,这是一个加在解释器上的全局(从解释器的角度看)锁(从互斥或者类似角度看)。这种方式当然很安全,但是它有一层隐含的意思(Python初学者需要了解这个):对于任何Python程序,不管有多少的处理器,任何时候都总是只有一个线程在执行。

Many discover this fact by accident. Newsgroups and message boards are littered with messages from Python novices and experts alike asking "why does my newly multi-threaded Python program run slower than when it had only one thread?" Many feel silly even asking the question, since of course a program with two threads where before there was just one will be faster (assuming that the work is indeed parallelizable). In fact, the question is asked so frequently that Python experts have crafted a standard answer: "Do not use multiple threads. Use multiple processes." But this answer is even more confusing than its question. I shouldn't use multiple threads in Python? How can multi-threading in a language as popular as Python be so broken as to have experts recommending against its use? Surely I'm missing something?

Sadly, nothing has been missed. Due to the design of the Python interpreter, using multiple threads to increase performance is at best a difficult task. At worst, it will decrease (sometimes significantly) the speed of your program. A freshman CS undergrad could tell you what to expect when threads are all competing for a single shared resource. The results are often not pretty. That said, there are many times that multi-threading works well, and it is perhaps a testament to both the interpreter implementation and the core developers that there are not more complaints about Python's multi-threading performance.

许多人都是偶然发现这个事实的。网上的很多讨论组和留言板都充斥着来自Python初学者和专家的类似这样的问题——”为什么我全新的多线程Python程序运行得比其只有一个线程的时候还要慢?“许多人在问这个问题时还是非常犯晕的,因为显然一个具有两个线程的程序要比其只有一个线程时要快(假设该程序确实是可并行的)。事实上,这个问题被问得如此频繁以至于Python的专家们精心制作了一个标准答案:”不要使用多线程,请使用多进程。“但这个答案比那个问题更加让人困惑。难道我不能在Python中使用多线程?在Python这样流行的一个语言中使用多线程究竟是有多糟糕,连专家都建议不要使用。难道我真的漏掉了一些东西?

很遗憾,没有任何东西被漏掉。由于Python解释器的设计,使用多线程以提高性能应该算是一个困难的任务。在最坏的情况下,它将会降低(有时很明显)你的程序的运行速度。一个计算机科学与技术专业的大学生新手可能会告诉你当多个线程都在竞争一个共享资源时将会发生什么。结果通常不会非常理想。很多情况下多线程都能很好地工作,可能对于解释器的实现和内核开发人员来说,没有关于Python多线程性能的过多抱怨。

What Now? Panic?

So what, then, can be done? Are we as Python developers meant to give up the idea of using multiple threads to exploit parallelism? Why does the GIL need to guarantee only one thread is running at a time anyway? Couldn't finer-grained locks be added to protect individual objects from simultaneous access? And why has no one attempted something like this before?

These are useful questions with interesting answers. The GIL protects access to things like the current thread state and heap allocated object for garbage collection. There is nothing special about the Python language, however, that requires the use of a GIL. It is an artifact of the implementation. There are alternative Python interpreters (and compilers) that do not make use of a GIL. For CPython, though, it's been there pretty much since the beginning.

So why not get rid of it? Many are not aware, but this was attempted back in 1999 for Python 1.5 in the oft-cited but poorly understood "free threading" patches from Greg Stein. In the patches, the GIL was completely removed and replaced with finer grained locking. Its removal, however, came at the expense of execution speed for single-threaded programs. It was perhaps 40% slower when running with a single thread. Two threads showed an increase in speed, but beyond that the benefits did not scale linearly with the number of cores. Because of the degradation in execution speed, the patches were rejected and largely forgotten.

现在该怎么办?惊慌?

那么,这又能怎样?问题解决了吗?难道我们作为Python开发人员就意味着要放弃使用多线程来探索并行的想法了?为什么无论怎样,GIL需要保证只有一个线程在某一时刻处于运行中?难道不可以添加细粒度的锁来阻止多个独立对象的同时访问?并且为什么之前没有人去尝试过类似的事情?

这些实用的问题有着十分有趣的回答。GIL对诸如当前线程状态和为垃圾回收而用的堆分配对象这样的东西的访问提供着保护。然而,这对Python语言来说没什么特殊的,它需要使用一个GIL。这是该实现的一种典型产物。现在也有其它的Python解释器(和编译器)并不使用GIL。虽然,对于CPython来说,自其出现以来已经有很多不使用GIL的解释器。

那么为什么不抛弃GIL呢?许多人也许不知道,在1999年,针对Python 1.5,一个经常被提到但却不怎么理解的“free threading”补丁已经尝试实现了这个想法,该补丁来自Greg Stein。在这个补丁中,GIL被完全的移除,且用细粒度的锁来代替。然而,GIL的移除给单线程程序的执行速度带来了一定的代价。当用单线程执行时,速度大约降低了40%。使用两个线程展示出了在速度上的提高,但除了这个提高,这个收益并没有随着核数的增加而线性增长。由于执行速度的降低,这一补丁被拒绝了,并且几乎被人遗忘。

The GIL is Hard. Let's Go Shopping!

The "free threading" patches are instructive, though, in that they demonstrate a fundamental point about the Python interpreter: removing the GIL is hard. Since the time of the patches, the interpreter has come to rely on more global state, making the removal of today's GIL that much more difficult. It should be noted that it is precisely for this reason that many become interested in attempting to remove the GIL in the first place; hard problems are fun.

But perhaps this is all a bit misguided. Let's consider what would happen if we had a magical patch that removed the GIL with no performance penalty to single threaded Python code. We would have what we said we wanted all along: a threading API that properly makes use of all processors simultaneously. Now that we've got what we want, is it actually a good thing?

移除GIL非常困难,让我们去购物吧!

(译者注:XXX is hard. Let's go shopping!在英语中类似于中文的咆哮体。其隐含意思为想成功完成某件事情非常困难,我们去直接寻找第三方的产品替代吧。)

不过,“free threading”这个补丁是有启发性意义的,其证明了一个关于Python解释器的基本要点:移除GIL是非常困难的。由于该补丁发布时所处的年代,解释器变得依赖更多的全局状态,这使得想要移除当今的GIL变得更加困难。值得一提的是,也正是因为这个原因,许多人对于尝试移除GIL变得更加有兴趣。困难的问题往往很有趣。

但是这可能有点被误导了。让我们考虑一下:如果我们有了一个神奇的补丁,其移除了GIL,并且没有对单线程的Python代码产生性能上的下降,那么什么事情将会发生?我们将会获得我们一直想要的:一个线程API可能会同时利用所有的处理器。那么现在,我们已经获得了我们希望的,但这确实是一个好事吗?

Thread based programming is hard. There are no two ways about it. Every time one thinks he or she understands everything there is to know about how threading works, a new wrinkle is uncovered. A number of high-profile language designers and researchers have come out against the threading model because it is simply too difficult to get right with any reasonable degree of consistency. As anyone who has written a multi-threaded application can tell you, both developing and debugging are exponentially more difficult compared to a single threaded program. The programmer's mental model, while well suited for sequential programs, just doesn't match the parallel execution model. The GIL, then, unintentionally serves to help protect a programmer from his or her self. While synchronization primitives are still required when using threads, the GIL actually helps preserve consistency of data between threads.

It seems, then, that Python's most difficult question may be asking the wrong thing. There's a good reason that Python experts recommend using multiple processes instead of multiple threads, and it's not to hide the inadequacies of the Python threading implementation. Rather, it is to encourage developers to use a safer and more straightforward concurrency model and reserve multi-threaded programming for when it is absolutely necessary. To many, it is not clear what, if any, is the "best" model for parallel programming. What is clear to most, however, is multiple threads is not it.

基于线程的编程毫无疑问是困难的。每当某个人觉得他了解关于线程是如何工作的一切的时候,总是会悄无声息的出现一些新的问题。因为在这方面想要得到正确合理的一致性真的是太难了,因此有一些非常知名的语言设计者和研究者已经总结得出了一些线程模型。就像某个写过多线程应用的人可以告诉你的一样,不管是多线程应用的开发还是调试都会比单线程的应用难上数倍。程序员通常所具有的顺序执行的思维模恰恰就是与并行执行模式不相匹配。GIL的出现无意中帮助了开发者免于陷入困境。在使用多线程时仍然需要同步原语的情况下,GIL事实上帮助我们保持不同线程之间的数据一致性问题。

那么现在看起来讨论Python最难得问题是有点问错了问题。我们有非常好的理由来说明为什么Python专家推荐我们使用多进程代替多线程,而不是去试图隐藏Python线程实现的不足。更进一步,我们鼓励开发者使用更安全更直接的方式实现并发模型,同时保留使用多线程进行开发除非你觉的真的非常必要的话。对于大多数人来说什么是最好的并行编程模型可能并不是十分清楚。但是目前我们清楚的是多线程的方式可能并不是最好的。

As for the GIL, don't think it just sits there static and unanalyzed. Python 3.2 saw a new GIL implementation by Antoine Pitrou with encouraging results. It was the first major change to the GIL since 1992. The change is too large to explain here, but at a high level, the old GIL counted Python instructions to determine when it was time to give up the GIL. As it turns out, a single Python instruction can comprise a large amount of work, as they don't translate 1:1 to machine instructions. In the new GIL, a hard timeout is used to instruct the current thread to give up the lock. When a second thread requests the lock, the thread currently holding it is compelled to release it after 5ms (that is, it checks if it needs to release it every 5ms). This leads to more predictable switching between threads when work is available.

It is not a perfect change, however. Perhaps the most active researcher into the effect of the GIL on various types of work is David Beazley. In addition to what is likely the most in-depth look at the pre-3.2 GIL, he has researched the new GIL implementation and discovered a number of interesting program profiles for which even the new GIL performs quite poorly. He continues to drive the discussion surrounding the GIL forward with practical research and published results.

至于GIL,不要认为它在那的存在就是静态的和未经分析过的。Antoine Pitrou 在Python 3.2中实现了一个新的GIL,并且带着一些积极的结果。这是自1992年以来,GIL的一次最主要改变。这个改变非常巨大,很难在这里解释清楚,但是从一个更高层次的角度来说,旧的GIL通过对Python指令进行计数来确定何时放弃GIL。这样做的结果就是,单条Python指令将会包含大量的工作,即它们并没有被1:1的翻译成机器指令。在新的GIL实现中,用一个固定的超时时间来指示当前的线程以放弃这个锁。在当前线程保持这个锁,且当第二个线程请求这个锁的时候,当前线程就会在5ms后被强制释放掉这个锁(这就是说,当前线程每5ms就要检查其是否需要释放这个锁)。当任务是可行的时候,这会使得线程间的切换更加可预测。

然而,这并不是一个完美的改变。对于在各种类型的任务上有效利用GIL这个领域里,最活跃的研究者可能就是David Beazley了。除了对Python 3.2之前的GIL研究最深入,他还研究了这个最新的GIL实现,并且发现了很多有趣的程序方案。对于这些程序,即使是新的GIL实现,其表现也相当糟糕。他目前仍然通过一些实际的研究和发布一些实验结果来引领并推进着有关GIL的讨论。

Regardless of one's personal feelings about Python's Global Interpreter Lock, it remains the language's most difficult technical challenge. To understand its implications requires a thorough understanding of operating system design, multi-threaded programming, C, interpreter design, and the CPython interpreter implementation. Those prerequisites alone preclude many developers from investigating it more thoroughly. Nevertheless, the GIL shows no signs of going away any time soon. For the time being, it will continue to both confuse and surprise those new to the language while simultaneously intriguing those interested in trying to solve very difficult technical problems.

The preceding is based on my research to date into the Python interpreter. While there are many other parts of the interpreter I hope to write about, none is more well known than the Global Interpreter Lock. The technical details were researched thoroughly against the CPython repository tip, though I imagine there are some inaccuracies. If you spot one, please let me know so that I may correct it as quickly as possible.

不管某一个人对Python的GIL感觉如何,它仍然是Python语言里最困难的技术挑战。想要理解它的实现需要对操作系统设计、多线程编程、C语言、解释器设计和CPython解释器的实现有着非常彻底的理解。单是这些所需准备的就妨碍了很多开发者去更彻底的研究GIL。虽然如此,并没有迹象表明GIL在不久以后的任何一段时间内会远离我们。目前,它将继续给那些新接触Python,并且与此同时又对解决非常困难的技术问题感兴趣的人带来困惑和惊喜。

以上内容是基于我目前对Python解释器所做出的研究而写。虽然我还希望写一些有关解释器的其它方面内容,但是没有任何一个比全局解释器锁(GIL)更为人所知。虽然我认为这里有些内容是不准确的,但是这些技术上的细节与CPython的很多资源条目是不同的。如果你发现了不准确的内容,请及时告知我,这样我就会尽快对其进行改正。

返回顶部
顶部