Thread safety now and in the future (no-gil)

Hello.

I’ve read PEP 703 and tried to understand what disabling GIL in future releases means for thread safetiness and how to writing code today which will also run flawlessly when GIL is disabled.

Would it be safe to read elements from list or dict while other threads may assign new values to already exiting elements?

I’m explicitly not asking about adding or removing elements from those datatypes but simply assigning a new value to an already existing element. It also doesn’t matter if the reading thread gets the value before or after the change but simply if integrity it affected.

For example would the following be safe without using explicit locks if the assignments are related to already existing elements? Item_1 is of type ‘list’ and item_2 is of type ‘dict’ here.

item_1[3] = ‘red’
item_2[‘color’] = 'blue'

I would assume that this code does not show any problems today if GIL is enabled.

Thanks for your comments.

Yes, it’s safe to read elements of a list or dict while another thread is modifying them concurrently. You’ll get either the old value or the new value.

3 Likes

Seems to me that one thread might delete the wanted entry even in a case like

if ‘a’ in mydict: z = mydict[‘a’]

or

if len(L)>=2: z = L[1]

although I agree that deletion is an extreme modification

The question was specifically about modifications that don’t change membership. Once membership changes are introduced, it’s thread unsafe regardless of the GIL or lack thereof.

One thing we’ve been thinking about lately for documentation is drawing a distinction between thread safety, sequential consistency, and atomicity.

IMO, saying that an operation is thread-unsafe makes people assume incorrectly that the interpreter will crash or undefined behavior in C code might be triggered.

Instead, in cases like this where there are no data races but where an algorithm is inherently not deterministic,
it’s better to refer to the code as “not sequentially consistent”. For an operation that might race (in Python) but still not lead to any C unsafety, you could refer to the operation as “non-atomic”.

I think reserving “thread-unsafe” for situations where the interpreter might crash or a C data race is happening makes it clear that non-atomic operations and non-sequentially-consistent implementations are still valid Python. That said, the code might still not be fit for purpose depending on the multithreaded application, or might need external synchronization to ensure consistency.

6 Likes

That is a good idea, but I think in practice we’ll need even more detail than that. As in, people will want specific guarantees about “if you do X and then Y then the result may be A, B, or C but not D” (where the variables stand for particular operations like somedict['key'] = 2, somedict['key'], etc.). Unless very strong guarantees will be made that allow a wide range of operations, people will want weaker guarantees that at least assure them they can do certain things and count on only having to worry about certain possible outcomes.

1 Like

While that might work for being precise in the technical documentation, I think it’s a lost cause for general usage. It doesn’t matter how much you might want things to be otherwise, people will tend to assume that if you say something is “thread safe” it means “will work properly with threads”. Finding out that it doesn’t crash the interpreter but gives incorrect results will lead them to feel misled - even if you explain the terminology to them.

To put this another way, I think we need to stop using the term “thread safe” to mean “the interpreter won’t crash”. Find a different term for that case - one that doesn’t have unwanted implications in people’s minds.

If we explicitly avoid giving “thread safe” a technical meaning, we can then always respond to a question about thread safety by saying “thread safe is an ambiguous term - could you clarify which of the following behaviours you actually mean?” That will probably get tiresome fast, but at least it won’t be actively misleading…

10 Likes

This is of course very tricky because there is no general meaning of what is “correct” if you have shared mutable state. If you modify state in one thread and modify or read from it in another then what behaviour did you want?

Maybe “memory safe” is better? Then memory safe means that reference counts won’t be corrupted, no null pointer exceptions, no double free or segfaults. If one thread empties the list while another is reading you might get IndexError but the memory is safely intact so you can catch the exception and continue trying something else.

5 Likes

As a concrete example of the confusion, the Library and Extension FAQ’s question What kinds of global value mutation are thread-safe? actually lists operations on objects of built-in type that are guaranteed to be atomic.

(See also another post of mine where I mention some other guarantees that aren’t documented.)

I’m surprised that D1.update(D2) is considered atomic in the FAQ as a quick test with 3.14.0 on my machine indicates that updating an empty dict with another of length 100000000 can take more than a second.

I think that both D1 and D2 will be locked for that whole second and so nothing else can operate on either of them for the whole operation. (I.e. “atomic” isn’t promising anything about “fast”, just “as-if a single operation”)

2 Likes

Oh, I really did not expect such a short and clear answer to my question.

Thank’s a lot for that.

That was exactly my problem coming from C++.

For people quite new to Python and coming from other languages with a lower abstraction level it is quite difficult to get an impression what are threadsafe operations in Python. I think one of the reasons for that is, if you are used to code in C++ you have a quite good understanding of what’s happening on machine level and which operations could be considered atomic and which not.

So it would have helped me lot if there were some kind of sheet at a prominent place in the docs which shortly shows some examples of dos and don’ts.

5 Likes

Indeed, it looks confusing at least to me.

In the example it is stated that the following is threadsafe.

D[x] = y

And in the next paragraph I can read

Operations that replace other objects may invoke those other objects’ __del__() method when their reference count reaches zero, and that can affect things. This is especially true for the mass updates to dictionaries and lists. When in doubt, use a mutex!

After reading this I’m in doubt for sure at least as a newbee.

You shouldn’t be in doubt because the second paragraph that you’re quoting isn’t saying anything about thread safety, it’s just saying that you might not get the results you expect (due to race conditions, etc.) The language is vague (which is not ideal) but it doesn’t suggest in any way that the interpreter would crash.

As I said above, people don’t have consistent expectations of what the term “thread safe” means. And the way it’s used in the Python documentation (won’t crash the interpreter) is IMO the least useful of all possible interpretations. Frankly, I view the idea that you can’t crash the interpreter by using Python code in threads as not worthy of its own term[1]. It’s simply “there’s no bugs in the interpreter”.


  1. Much less a widely-used term like “thread safe” ↩︎

5 Likes

I think this is important to underline. One point I’ve been considering is that we don’t generally document all the ways you can misuse/abuse Python code, or all the ways that things could go wrong, and it feels odd to be considering introducing lots of caveats in the documentation related to threading given this history.

Perhaps we will gain higher level concurrency abstractions that help here, but I don’t want the docs to be off-putting or unduly verbose at the expense of runtime problems. It should be easy to do the right thing, and harder (but possible) to do everything else.

A

1 Like

Agreed. Other than ctypes, I can’t think of any issue I’ve seen in the last 25 years where an interpreter crash hasn’t been considered a bug in the interpreter (and not in the user’s code). Sure, we can’t or don’t fix all of those, but we acknowledge they’re bugs and we’d fix them if we could. I’d think the same thing about threading-related crashes.

1 Like

IMO, it’s unfortunate that the publicity around free threading has made it seem like it will be of more immediate benefit than it actually will be in practice (for the average Python user). Personally, I think that implementing higher level concurrency abstractions[1] is essential if we want people to view free threaded Python as a success.

To put this another way, if free threading ends up encouraging users to use more manual locking, we’ll have failed in our goals of making multi-threaded Python user-friendly.


  1. For example something to make using the map/reduce pattern with concurrent.futures more natural than the “obvious” approach of using a shared global result accumulator. ↩︎

4 Likes

What I have taken from you answer and all the other useful posts here is, that it would really be helpful to have a document about “safely using threads in Python” which at first has a glossary which defines the terms it uses at least for the scope of that document.

Secondly, due to the nature of language, which tends to getting comprehensive and difficult to understand when trying to rule out any kind of false assumption, it would be helpful to provide examples which are as short as possible, but as detailed as necessary, to understand the context for which they are valid.

Just my 2 cents.

1 Like

For what it’s worth, @lys.nikolaou, @willingc and I sat down at a table during an in-person Quansight all-hands meeting last week and wrote the outline for a set of multithreaded programming docs that would include this.

We’re still working out a detailed proposal that we’ll take to the docs team and other stakeholders. Our goal is to have a full set of docs using new content and content from the existing free-threaded guide with a timeline of shipping alongside Python 3.15 next year. At the same time we’ll also be looking at standards and best practices for documenting thread safety in libraries along with updates for documentation tooling if that’s needed.

Thanks all for the input about terminology and what is needed in the docs, it’s really helpful.

9 Likes