翻译于 2013/06/05 15:34
3 人 顶 此译文
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.
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?"
随处都是问题。难度大、耗时多肯定是其中一个问题。仅仅是尝试解决这个问题就会让人惊讶。之前是整个社区的尝试，但现在只是外围的开发人员在努力。对于新手，去尝试解决这样的问题，主要是因为问题难度足够大，解决之后可以获得相当的荣誉。计算机科学中未解决的 P = NP 就是这样的问题。对此如果能给出多项式时间复杂度的答案，那简直就可以改变世界了。Python最困难的问题比证明P = NP要容易一些，不过迄今仍然没有一个满意的解决，要知道，这个问题的实用的解决方案同样能起着变革性的作用。正因为如此，很容易看到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.
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.
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.
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.
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.
那么为什么不抛弃GIL呢？许多人也许不知道，在1999年，针对Python 1.5，一个经常被提到但却不怎么理解的“free threading”补丁已经尝试实现了这个想法，该补丁来自Greg Stein。在这个补丁中，GIL被完全的移除，且用细粒度的锁来代替。然而，GIL的移除给单线程程序的执行速度带来了一定的代价。当用单线程执行时，速度大约降低了40%。使用两个线程展示出了在速度上的提高，但除了这个提高，这个收益并没有随着核数的增加而线性增长。由于执行速度的降低，这一补丁被拒绝了，并且几乎被人遗忘。
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?
（译者注：XXX is hard. Let's go shopping!在英语中类似于中文的咆哮体。其隐含意思为想成功完成某件事情非常困难，我们去直接寻找第三方的产品替代吧。）
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.
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.