CPython is known for having a Global Interpreter Lock (GIL), which many users consider to be detrimental. What does this mean, and why would an intepreter be designed to have this feature?
-
7$\begingroup$ When you vote down, please specify in comments what is wrong. $\endgroup$– чистов_nCommented Jul 2, 2023 at 9:19
-
10$\begingroup$ I would imagine top reason for the downvotes is that the top internet search results easily answer your question - suggesting lack of research on your part. Your question does not add any context to explain why those explanations (if you found them) are not sufficient for you. $\endgroup$– Bruce AdamsCommented Jul 2, 2023 at 10:06
-
10$\begingroup$ @Starship-OnStrike That's not a reason to close a question $\endgroup$– mousetailCommented Jul 2, 2023 at 13:22
-
11$\begingroup$ I think this question is fine; I’ve voted to reopen. It is clearly about programming language implementation, and it has a precise and well-defined answer (which cannot be said of many other questions asked recently with a much higher score). It is true that information can be found easily via an internet search, but having information on essential concepts here is useful as a repository of knowledge. $\endgroup$– Alexis King ♦Commented Jul 2, 2023 at 16:15
-
10$\begingroup$ 8 downvotes? Y'all, that's excessive. I've seen spam with better ratios than this. $\endgroup$– rydwolfCommented Jul 2, 2023 at 17:13
2 Answers
As the question notes, the specific term “global interpreter lock” is generally associated with CPython, which infamously includes a global mutex that must be acquired whenever any thread interprets Python code. CPython was designed before multicore processors were a common occurrence on commodity hardware, so support for parallelism was not engineered into the runtime from the start. This decision turned out to be difficult to undo:
CPython’s interpreter includes global data structures shared between threads. Some of these data structures are heavily accessed and modified during execution, so fine-grained locking of each access would be prohibitively expensive.
CPython provides a relatively strong memory model to Python programs, so explicit barriers are not necessary for mutation to be well-ordered from the perspective of other Python threads, which would be difficult and expensive to guarantee without the GIL.
Much of CPython’s popularity stems from its easy interoperation with C libraries, many of which are not themselves thread-safe, so elimination of the GIL would create a compatibility headache for those libraries.
Some other interpreted languages include a GIL or something like it, most notably the primary Ruby implementation, MRI. However, even among languages that don’t provide parallel threads, most implementations don’t include a GIL per se.
Racket, for example, is essentially a single-core language in much the same way CPython is: it provides preemptively multitasked green threads, but these only run concurrently, not in parallel. However, Racket does not have a global mutex that locks the interpreter, and there is relatively little global interpreter state. Instead, Racket’s thread scheduler simply chooses to run a single thread at a time, so all program execution is still serialized. Even in the absence of a GIL, there are reasons why Racket chooses to serialize execution:
Like CPython, Racket provides a strong memory model to programs, so mutations on one thread are always visible in the same order from the perspective of another.
Like CPython, Racket provides an FFI, and many programs use C libraries that are not completely thread-safe.
Even though Racket doesn’t have a GIL, it provides an unsafe primitive to enter “atomic mode”, during which the thread scheduler will never switch threads, which is used to implement some low-level functionality. This would be impossible to implement efficiently with parallel threads.
In practice, this means that Racket is limited in much the same way as CPython from the perspective of most programmers, so saying that Racket “doesn’t have a GIL” might seem like a technicality. However, there is a real difference: the obstacles to providing parallel threads in Racket are essentially just backwards compatibility constraints, while a GIL is a fundamental technical limitation. Indeed, safe parallelism is available in a restricted form, and recent versions of Racket provide unsafe access to OS threads without issue, which would not be possible if Racket had a GIL.
As this answer hopefully illustrates, the main reason for a language to have a GIL is one of backwards compatibility. Adding support for parallel execution to an existing language can be quite challenging; the best way to avoid falling into a similar trap is to design a language and its runtime to support parallelism from the start. Many older languages did not do this because shared-memory parallelism was simply not widely available when they were designed.
Personally, I think it is worth taking the time and effort to ensure new languages can support fine-grained parallelism eventually, rather than backing oneself into a corner. Multicore systems are more or less ubiquitous now, and programmers wish to take advantage of them. However, there is undoubtedly work involved in doing this, so whether it is worth it to you will always be a matter of opinion.
-
$\begingroup$ so fine-grained locking of each access would be prohibitively expensive. - And might lead to inconsistent results if two threads were accessing the same data structure and parts of the changes from one Python statement became visible to other threads while others didn't yet. (I guess that would require Python programs to do their own locking around any modifications to shared state, which is probably necessary anyway in a lot of cases, so IDK how much they benefit from whatever atomicity the GIL gives.) $\endgroup$ Commented Jul 2, 2023 at 20:09
-
$\begingroup$ explicit barriers are not necessary for mutation to be visible from other Python threads - Note that hardware has coherent caches between cores that run threads of the same program, so visibility isn't much of a problem; as long as data gets stored to memory (not kept in a register by a compiler) it will be visible to other threads. $\endgroup$ Commented Jul 2, 2023 at 20:14
-
$\begingroup$ Memory barriers are needed for ordering, to make operations on data structures visible in a consistent order, e.g. as part locking to give the acquire and release semantics a lock needs to serialize the critical sections of different threads. And for a non-trivial data structure like a hash table or Python List or integer, the different parts of the C data structures need to be consistent with each other, so what you said is true about making mutations visible in a safe way, that other threads can read without seeing objects in mid modification. (Or having C data-race undefined behavior) $\endgroup$ Commented Jul 2, 2023 at 20:16
-
$\begingroup$ The way you put it does basically give the right idea, that an interpreter designed around serial execution of its functions that modify data structures can use a GIL to preserve that and still be able to use threads for I/O parallelism (e.g. so they can block in a system call without holding the lock). It's non-obvious why Racket would be able to lets its interpreter functions run in parallel, other than I guess having "relatively little global interpreter state". $\endgroup$ Commented Jul 2, 2023 at 20:25
-
1$\begingroup$ @PeterCordes Yes, you’re right, my wording was sloppy—I’ve updated the answer to be a little more explicit. As for why Racket is able to let its interpreter run in parallel—why do you think there must be some obstacle? There is no trouble to running multiple parallel mutators as long as they have some simple protocol to yield for garbage collection. $\endgroup$– Alexis King ♦Commented Jul 2, 2023 at 20:44
Needing a global lock is a consequence of an ad hoc approach to implementing an interpreter, and is perfectly avoidable.
If your language have, say, dynamic scoping, any thread of the interpreter may want to query a global identifier table at any moment. GIL is an easy way to ensure there are no race conditions in accessing the table. This could have been avoided, firstly, by not designing a language with dynamic scoping (really, why would anyone do it?), and, secondly, if you're really hell-bent on having dynamic name lookup in runtime, by using discrete lock-free data structures for such tables.
And dynamic name lookup is just one of the parts of the CPython implementation that needs to be protected from race conditions.
To summarise: needing a GIL is pretty much a consequence of poor choices in both language design and in interpreter implementation.
-
1$\begingroup$ I think this answer is using the term "dynamic scoping" to mean something different to what it is normally understood as meaning. I would think that dynamic scoping, i.e. resolving names by traversing the call stack instead of lexically, would mean different threads (each having their own call stack) need not contend for a any shared resource when resolving names. $\endgroup$– kaya3Commented Jul 2, 2023 at 17:24
-
$\begingroup$ @kaya3-supportthestrike Python, despite claims of the opposite, use dynamic scoping and late binding in this very sense. Any thread can look.up outer definitions of the same binding. Not that different from elisp and its ilk. $\endgroup$– SK-logicCommented Jul 2, 2023 at 17:42
-
1$\begingroup$ Python has lexical scoping for most variables, the exceptions being global variables defined dynamically through the
globals()
dictionary, and arguably the ability to walk the call-stack at runtime using theinspect
module. If either of those is what you mean by "dynamic scope" then you should make this clear (though I don't see how either lead to a resource contention between threads that lexical scoping wouldn't have). Python of course has late binding, but that is a different thing. $\endgroup$– kaya3Commented Jul 2, 2023 at 17:58 -
$\begingroup$ I don’t think this answer accurately describes why CPython has retained its GIL, or at least I think it paints a misleadingly incomplete picture. If the GIL were purely an implementation problem, it would be removed by now, but the GIL has become a part of CPython’s interface. $\endgroup$– Alexis King ♦Commented Jul 2, 2023 at 18:17
-
$\begingroup$ Yes, I said that name lookup is just one of the things GIL was needed for, CPython grew dependent on it in many other aspects to an extend that removing GIL will be a breaking change in an interface, so now it is virtually impossible to fo. As I said, bad design choices early on, doomed to linger forever now. $\endgroup$– SK-logicCommented Jul 2, 2023 at 19:00