In discussion of Sam Gross’s noGIL fork of Python 3.12 (happening in other groups here and here), I’ve seen a few comments which make me think some folks don’t really understand thread safety. In fact, I think some people might believe their code is thread-safe because the GIL is present. That’s incorrect, and I’d like to open a discussion here to try and bring everybody up to a common baseline level of what it means to be thread-safe. Disclaimer: I am no threading guru. I’ve used it a bit over the years, generally in a producer/consumer context where I can simplify my life by using queue.Queue
as the communication channel. Every now and then I might need to lock something. I can’t recall ever having to lock two separate bits of data (two locks, acquired at different times). Note that I am only concerning myself with thread programming at the Python level.
To start with, I have a mental model of thread safety which contains two basic elements (there might well be more):
- No shared data is accessed by a thread which hasn’t first locked it appropriately.
- There are no deadlocks in the code (this is the “two lock” situation I alluded to above).
This topic has been discussed here before. In that thread, @steven.daprano provides a concrete example of what can go wrong when access to a shared bit of data isn’t properly mediated. That’s worth reading. Steven’s example of x += 1
is an excellent of demonstration of my item #1.
Let me expand on the deadlock idea a little. Suppose we have two threads, A and B, each of which wants to manipulate x
and y
. For some (probably not so) strange reason, thread A generally discovers it needs to manipulate x
first, then sometime later (say, after many bytecodes have been executed), discovers it needs to also fiddle with y
. In contrast, thread B always wants to access y
first, then migh later discover it needs to also manipulate x
. Flow of control might look something like this:
- Thread A gets control, locks
x
- Electrons are shuffled back-and-forth for a bit
- Thread B gets control, locks
y
- Electrons are shuffled back-and-forth for a bit
- Thread A regains control, discovers that it really wants to also work with shared object
y
, so attempts to acquire a lock for it - The attempt by A to lock
y
causes it to lose control of the virtual machine - Thread B regains control, executes for a bit, then decides it really wants to also work with shared object
x
, so attempts to acquire a lock for it
Oops. We have deadlocked. Neither A nor B can acquire the second lock they want, because the other thread is holding it. My purpose here isn’t to solve the deadlock problem, just to note that it exists when you don’t exercise some care.
I’m sure there are other examples of threading problems which can occur, but I suspect that most are assembled in various ways from these two basic problems, access to shared data and deadlock.
Notice that neither the simpler example of two threads modifying a single shared object, nor the deadlock (deadly embrace) example says anything about the GIL. Both of these problems can (and do) occur in current Python code today. They are well understood issues when one writes multithreaded code, and existed long before Python did. An external library (in a separate thread someone mentioned that glib
’s setenv
function isn’t thread-safe) can have (and hopefully document) threading problems. Using them today requires proper safeguards at the Python level. @cameron provided pseudo-code in that other thread about how to deal with thread-unsafe code over which you have no control.
So, that’s my minimal contribution. A couple days ago I came across a set of slides from a talk given by Aahz at OSCON 2001. I had to resort to the Wayback Machine. Despite the fact that Python 1.5.2 was current at the time, it’s still worth your time if you’re just getting started with threads.