Could be that the impact of this change is far broader than just a few key libraries.
Could be that the impact of this change is far broader than just a few key libraries.
Most of the issues with multi-threading come from concurrency, not parallelism. The GIL allows concurrency, you just don’t get any of the advantages of parallelism, which is normally the reason for putting up with the complexity concurrency creates.
That thread behavior is enough to reduce the likelihood of races and collisions; particularly if the critical sections are narrow.
There's already a term for that: not thread-safe.
The definition of thread safety does not include theoretical or practical assessments regarding how frequent a problem can occurr. It only assesses whether a specific class of problems is eliminated or not.
Well, obviously.
The challenge I am putting forth on HN is to meaningfully describe _usable_ thread-unsafe software. If you've spent enough time outside university, you'll be aware that there are all kinds of theoretical race conditions that are not triggered in practical use.
Nothing obvious changed (it was still running a decade old JRE), perhaps it was a kernel security patch, perhaps a RAM was replaced or even just the runtime data increased/changed in some way which woke up this monster.
Fun fact, I actually do! It's from that perspective I wrote that: every time you perturb the software environment, a new set of bugs that didn't happen in the old env before arises.
Also, rare on one computer (or today's computer) might not be rare on another (tomorrows faster one for example).
These types of bugs are also very hard to detect. You might not know your data is corrupted. Reminds me of how bad calculations in excel has cost companies billions of dollars, except now, the calculations could be "correct" and the error sitting dormant, just waiting for the right timings to happen. Much better to not make assumptions about the safety and think about it up front: if you are using multiple threads, you need to carefully consider your thread safety.
There is no such thing as "rare enough". Random or probabilistic bugs are one of the worst things software can have.
Why not just set it to auto reboot every week, that seems to fix it - right?
I don't know how the point of the comment could be missed, but what I am saying is, it is a mistake, a rookie baby not-a-programmer not even any kind of engineer in any field, to even think in those sorts of terms at all. At least not in the platonic ideal worlds of math or code or protocol or systems design or legal documents, etc.
Physical events have probability that is unavoidable. How fast does the gas burn? "Probably this fast"
There is no excuse for any coder to even utter the word "likely".
The ONLY answers to "Is this operation atomic?" or "Is this function correct?" or "Does this cpu perform division correctly?" Is either yes or no. There is no freaking "Most of the time."
"Likely" only exists in the realm of user data and where it is explicitly created as part of an algorythm.
You cannot guarantee your public key algorithm is impossible to break, but you can use keys long enough that an attacker has an arbitrarily low chance of succes with the best known methods.
You cannot prove your program is bug free, outside of highly specialized fields like aircraft control, but you can build a multi-layered architecture that can reduce the likelihood of successful intrusion. You cannot prevent a EMP bomb from wiping all your hard-drives at once, but you will likely maintain integrity of your database for uncorrelated hardware errors.
"Likely" is a tool that works in the real world. If you will chase mathematic certainty, your competition will likely eat your lunch.
Where you might be correct is that "unlikely" is very close to "likely" in the particular topic of thread safety, you just need a sufficiently large userbase with workloads and environments sufficiently different from your test setup.
Thread1: a = 0xFFFFFFFF00000000
Thread2: a = 0x00000000FFFFFFFF
One might think that the two possible values of a if those are run concurrently are 0xFFFFFFFF00000000 and 0x00000000FFFFFFFF. But actually 0x0000000000000000 and 0xFFFFFFFFFFFFFFFF are also possible because the load itself isnt atomic.
The GIL (AFAICT) will prevent the latter two possibilities.
The GIL exists to protect the interpreters internal data, not your applications data. If you access mutable data from more than one thread, you still need to your own synchronisation.
One example would be incrementing a counter for statistics purposes. If the counter is atomic, and the reader of the value is ok with a slightly out of date value, it's fine. If code is doing this in GIL Python, it's working now, and will break after the GIL is removed.
Atomics are isolated to where they are needed, the GIL is global though C (but not Python) code can release it.
https://stackoverflow.com/a/1717514
The Python documentation seems misleading to me on this:
> In theory, this means an exact accounting requires an exact understanding of the PVM bytecode implementation. In practice, it means that operations on shared variables of built-in data types (ints, lists, dicts, etc) that “look atomic” really are.
count = 0
def inc():
count += 1
sure "looks atomic" to me, so according to the documentation should be, but isn't.On the other hand, I think you could build a horribly inefficient actual atomic counter with
count = []
def inc():
count.append(None)
def get_count():
return len(count)
I think it's quite likely there's correct and non-horrible code relying on list append and length being atomic. Although it sounds like this might continue working without the GIL:> A lot of work has gone into the list and dict implementations to make them thread-safe. And so on.
So removing the GIL might not be a problem.
Regarding list, it sounds like it might actually keep working atomically without the GIL:
> A lot of work has gone into the list and dict implementations to make them thread-safe. And so on.
if
I know you came to the same conclusion in another comments, but here's a look at it using the `dis` module:
a += 1
turns into LOAD_FAST 1 (loads b)
LOAD_CONST 1 (loads the constant 1)
INPLACE_ADD (perform the addition)
STORE_FAST 1 (store back into b)
So if the interpreter switches threads between LOAD_CONST and STORE_FAST (so either before or after the INPLACE_ADD), you could clobber the value another thread wrote to `b`.From your other comment:
> so even though an increment on a loaded int is atomic, loading it and storing it aren't
Its always the loads and stores that are the problem.
But that's the problem with relying on the GIL: it has lock in the name, but it does not protect you, unless you understand the internals and know what you're doing. It protects the interpreter. This isn't much different from programming in other languages without a GIL: if you understand the internals what you're doing you may or may not need locks, because you will know what is and isn't atomic (and even when things are atomic, its still difficult to write thread-safe code! lock-free algorithms are much harder than using mutexes).
Thread safe code requires thinking hard about your code, the GIL does not protect you from that.
For instance, in your example, I know that the call to the C code to do the list operations is atomic (a single bytecode instruction), but I can't assume that all such calls to C code are safe because of this, unless I know for sure that the C code doesn't itself release the GIL. I assume that simple calls like list append/pop wouldn't have any reason to do this, but I can't assume this for any given function/method call that delegates to C, since some calls do release the GIL.
So, with or without GIL, you either really need to understand what's going on under the hood so you can avoid using locks in your code (GIL or atomics-based lock-free programming), or you use locks (mutex, semaphore, condition variables etc). No matter what you do, to write thread safe programs, you need to understand what you're doing and how things are synchronizing and operating. The GIL doesn't remove that need.
Of course, removing the GIL removes one possible implementation option, I just don't believe the GIL really makes it any easier. Once you know enough internals to know what is and isn't safe with the GIL, you could just as easily do your own syncrhonization.
So with Python concurrency, you can get unpredictable behavior (such as two threads losing values when incrementing a counter), but not undefined behavior in the C sense, such as use-after-free.
The compilers also take care to align most variables.
So while your scenario is not impossible, it would take some effort to force "a" to be not aligned, e.g. by being a member in a structure with inefficient layout.
Normally in a multithreaded program all shared variables should be aligned, which would guarantee atomic loads and stores.
Real life bugs have come from misapplication of correct parameters for memory barriers, even on x86. Python GIL removes a whole class of potential errors.
Not that I'm against getting rid of the GIL, but I'm more sceptical that it won't trigger bugs.
Though in my opinion python just isn't a good language for large programs for other reasons. But it'd be nice to be able to multithread some 50 line scripts.
The current ARM memory model is more relaxed regarding the ordering of loads and stores, but not regarding the atomicity of single loads and stores.
Some more discussion here: https://stackoverflow.com/questions/1717393/is-the-operator-...
Presumably this patch changes the list implementation in some way so that the extend operation remains thread-safe without the GIL.
Porting the thing to a multi-core system revealed that there were a lot of nasty concurrency bugs, like dead-locks or crashes which happened after a day of operation. And this wasn't a toy system - it was in use for a long time in an industrial application, and the customer was not too happy about the intermittent dead-locks. I commiserated with the poor engineer who had the quite stressful task to debug this, equipped with a lot of dedication but an insufficient background.
Frankly, while it would be nice to be able to write parallel code in pure Python, I think that Clojure with its purely-functional approach has the better concepts for this. And moreover, actually improving performance by parallel computation (using several CPUs to work in parallel on the same thing) is damn hard and unsolved in many cases (just come up with an efficient parallel Fast Fourier Transform and you might get a Turing award). What is mostly needed (outside of massive data processing pipelines) is concurrency for event-driven systems. Python can handle that, Clojure does handle it in a much more elegant way.
with gil:
call_a_method()
print(some_debugging_info)
with all the sit-ups you’d have to do in a “real” concurrent language.It only protects the state of the Python interpreter and that of C/Cython extension modules. Though even there, you can have unexpected thread switches, e.g. in Cython `self.obj = None` can result in a thread switch if the value previously stored in `self.obj` had a `__del__` method implemented in Python.
And AFAIK pretty much any Python object allocation can trigger the cycle collector which can trigger `__del__` on (completely unrelated) objects in reference cycles, so it's pretty much impossible to rely on the GIL to keep any non-trivial code block atomic.