How to know what is safe in threaded code

It would be really helpful if you could state what rules you used when checking the code for correctness and where you got those rules from.

This is exactly the kind of assumption I was looking for. Are there others you can think of for this example? Is there any reason to fear modifications to self.__dict__ or similar? Obviously changing what _thing_lock points to would be a problem but if we avoid that are we safe even if threads modifies the __dict__ while holding the lock?

I have not found any documentation that lays out even basic informal rules of what to expect so for me these examples are not trivial at all. I can reason fairly well about what goes on in the background but since I don’t know what rules I’m playing by I can only make somewhat educated guesses at if those things are safe or not. The threading module documentation is not very helpful in this regard since it only documents the basic API functions without discussing the actual programming model at all.

The API documentation that is there could also use some clarification regarding pre- and postconditions I think. Take Event.set as an example. It says:

Set the internal flag to true. All threads waiting for it to become true are awakened. Threads that call wait() once the flag is true will not block at all.

To me, it would be very helpful if this description included something like:

All updates done by the calling thread before the call are guaranteed to be consistent and visible before any waiting thread is unblocked.

Coming from other programming languages, this is not something I just assume unless it’s explicitly stated somewhere. I think similar clarifications would be helpful in a lot of the threading documentation.

1 Like

Does the documentation also need to state that, after you assign x = 1, x will be equal to 1? Unless there is a good reason to believe it might NOT be consistent, stop assuming the utter worst possible scenario.

Python is not C.

Assignments are fairly well documented as far as I can tell.

Yes, and does it claim that x will be equal to 1? If I sound insane and paranoid, realise that this is exactly how YOU sound when you mistrust Python at every step.

Python behaves sanely by default. You do not need to be told every single thing. Python’s policy is not “if it’s not documented, do whatever will be the most annoying and unpredictable thing possible”.

I don’t think this line of discussion contributes much to the topic but it seems like the documentation does exactly that:

If the target list is a single target with no trailing comma, optionally in parentheses, the object is assigned to that target.

There are very good reasons to be conservative and assume as little as possible when dealing with concurrency. Most languages leave the vast majority of thread interactions as undefined/illegal with unpredictable outcomes and only define a very narrow set of well defined/legal interactions. Python might different in this regard and that is fine. That “Python is not C” is not an issue, but python needs to define what it actually is in this aspect, otherwise it’s very difficult to use correctly. There are clearly many ways to write incorrect threaded programs so simply saying “You don’t need to know” is not helpful, I need some rules to distinguish valid code from invalid code.

1 Like

This says nothing about equality. Do you see what I mean about nitpicking the docs? This is exactly the sort of nitpicking that you have been doing. You are utterly paranoid about the threading module, complaining that it does not document its safety, but this is nothing to do with threading. Python’s execution model simply is not the way you’re describing, and none of this that you are worried about is going to happen.

Python is EASY to use correctly, because it BEHAVES CORRECTLY. That is how it is designed. Stop expecting Python to behave incorrectly.

Please show some examples. “Clearly” is a weasel word and not helpful here. Show some ACTUAL examples that are incorrect, and ask for help about those. Most likely, if the threading module says nothing about it, it is not a threading matter, and is a matter of Python’s execution model - whether single or multi threaded.

This is a lot more specific. The starting point for this topic was overly broad – you want me to explain how I know that an example is safe/correct, but it’s very hard to tell what you’re looking to have clarified. (e.g. Do I need to explain what a thread is and how locks work? Probably not useful.)

I think your priors are backwards here – you’re asking that objects which are designed for thread synchronization tediously spell out that they are thread safe. More documentation is not always better documentation. Threading related constructs (and multiprocessing constructs, similarly) should only include extra descriptions and warnings when there are known surprising behaviors.

Python is a high level language, not a systems language, and it isn’t in the habit of handing user code radioactive objects. So how do I know that Event never hands control back to user code while it’s internally inconsistent? Because it doesn’t document that it may do that.

3 Likes

I believe that the question is not about the event object itself, but the other updates that happen in the thread before the Event.set call, example:

bar = 2
def thread1():
    bar = 42
    event.set()

def thread2():
    event.wait()
    print(bar) # guaranteed to print 42?

Why would anyone think otherwise? Modern CPU can reorder instructions but the operations on CPU A must appear as if executed sequentially to CPU A. This requirement does not apply to other CPUs that execute concurrently with A, they can see memory operations done by A in different order.

2 Likes

I think I have a fairly solid understanding of the basics of threads and synchronization primitives, that’s not what I’m looking for. I was primarily thinking about how to rule out race conditions in python code. I was hoping someone could give a reasonable motivation for why the examples are free of races so that I can apply similar logic to other examples in the future.

Since the primary purpose of these objects is to provide synchronization I think it’s important that they document what they actually do in that regard. This is a complicated subject and clarity is important, synchronization can mean a lot of different things. I can’t think of any other example in the documentation where the main functionality is left undocumented and up to the reader to guess like this. Even trivial APIs like list.append clearly states what they do, even if that could be seen as obvious from context. Thread synchronization is several orders of magnitude more complicated and certainly deserves at least a sentence to clarify.

That Event objects are thread safe and maintain internally consistency say little about what guarantees are offered for your surrounding code. It’s probably fairly reasonable to assume some sort of memory barrier is implied but why do we need to assume when it could just be stated in the documentation what guarantees are actually provided? These guarantees are after all the main purpose of the object…

I have also been burned before by “quirks” where python does indeed do very strange things with very little in the way of warning in the documentation. First example I can think of is the very poor handling of threads in combination with multi-processing. This has only the smallest of hints in the documentation and it will almost certainly deadlock your application if you don’t know how to work around it on the affected platforms. It seems like some work is ongoing to improve this in the future but there is certainly precedent for surprising and poorly documented behaviour.

1 Like

Okay. Lemme offer you a few guarantees.

  1. After an Event, the integer 4 will not have suddenly become equal to 5.
  2. After an Event, a list will not begin behaving like a dictionary.
  3. After an Event, the random number generator will not be reseeded.
  4. Etcetera.

Why do you demand that everything be written out? As @sirosen said, you’re asking for the wrong thing. Assume by default that Python will behave sanely; that is, after all, how the language has been designed. Stop assuming that everything is out to get you.

>>> x = 100000000000.0
>>> x == x + 0.000000000001
True

is this “sane” or not? One has to understand IEEE 754 or at least its pitfalls like this one. For someone who doesn’t know IEEE 754 and its floating point arithmetic, this may be surprising. For someone who knows a bit about IEEE 754 floating point arithmetic, it would be surprising if the result was False.

Python could have “protected” the users from such pitfalls if it used arbitrary precision numbers. If that was the case, there would be perplexed people coming here and asking whether the code above really is guaranteed to return False on any supported platform for any x. Those would be people that know the intricacies of floating point arithmetics. They wouldn’t be satisfied with answer: it is sane behavior therefore we do not need to document it, nor with: some specific version of CPython compiled with some specific compiler on some specific HW for some specific x on some specific day gave False, so it is guaranteed to work.

Why doesn’t Python use arbitrary precision numbers? I guess historical reasons, but also:

  • simplicity of the implementation inside CPython
  • arbitrary precision numbers would be orders of magnitude slower

Can you try looking at the concurrency concerns raised here by similar optics?

You could argue that basic knowledge of IEEE 754 is something any developer who wants to do floating point calculations should know. Is memory ordering something that any developer who wants to write concurrent code (that would eventually run in parallel with no GIL) should know?

1 Like

Well, if you pick up a pocket calculator and try things, you’ll very quickly run into limitations of precision. It’s far easier to explain than “if you add 1 to 255, you get 0”, and lots of programming languages have that (or “add 1 to 127, you get a negative number”). Unless you want to demand infinite precision and processing power, you WILL have rounding errors.

Arbitrary precision still isn’t infinite. The only way to be completely accurate is fully symbolic arithmetic, where you keep notations like pi and square roots unevaluated. So that would just change the confusion to “why does this take so long to do basic arithmetic?”. Arbitrary precision works for integers since they can’t actually require infinite precision.

There certainly are some quirks to IEEE floating point, but “numbers get rounded” is hardly a reasonable one to complain about.

Mainly the second one, and by “orders of magnitude”, you really mean “arbitrarily slow”. We’re not talking “all arithmetic would take 1000 times as long”, we’re talking “arithmetic gets slower and slower the more of it you do”. Try calculating this without ANY rounding:

x = 2 ** 0.5 + 1/3 + 1/5 + 1/7 + 1/9 + 1/11 + 1/13
y = 5 ** -0.25 + 7 ** -0.125 + 9 ** 0.0625
print(x > y)

With any sort of floating-point fixed-size arithmetic, this can be calculated in a reasonable amount of time. With no rounding whatsoever, it’s probably impossible, but if you allow some kind of high precision root calculation, and then maintain arbitrary precision after that, you’re going to need a ridiculous amount of processing power to calculate.

So, no, I’m not seeing a similarity here. The most basic and obvious way to do ANY arithmetic is to limit your precision. This isn’t a oomputing question, it’s just a fundamental of our abiltiy to work with infinities.

Nope, I’m not. I will argue that any developer who wants to do floating point calculations should know that computers don’t have infinite amounts of memory, though.

No, it isn’t, but again, I would assume a few fundamental basics, like that stuff gets evaluated in a particular order. That order doesn’t depend on threading or other forms of concurrency, and it’s very well defined in Python.

And while a lot of Python programmers won’t know exactly what that order is, most at least wouldn’t be surprised by it. Stuff is evaluated left-to-right, unless there’s a good reason not to.

yes, in single thread, absolutely. We’re talking about reads and writes visibility and ordering between different CPUs, i.e., different (potentially parallel) threads of execution.

Hasn’t Petr Viktorin already those questions? The GIL prevents any (of those kind of) problems with threading, because there is no real threading in Python.

1 Like

Writes and reads are “side-effects” of evaluation. So the visibility of those effects is tied to the evaluation order. Nothing in the python spec allows this to be broken or AFAIK even gives you the indication that this might be broken. You can only imagine this to be broken by having advanced knowledge about how modern CPUs work. If you try to understand python without this knowledge, it is clear that the behavior you are imaging would be very unintuitive.

Yes, between different threads the order might be different, but within a single thread the order is well defined. So in this piece of code

bar = 2
def thread1():
    global bar
    bar = 42
    event.set()

It would be very weird within the framework of python that a side-effect of something evaluated late happens before the side-effect of the first statement. And AFAIK, nothing in the python documentation gives you the indication that this might be possible.

Do you just want a statement somewhere “code is executed in order”? Or what change do you actually want to have in the docs? And where? (note that the GIL is a somewhat unrelated topic)

…of a sort, yes? And it should be in the language specification.

For example you can find C#’s version of exactly what op is asking for in section 7.10 of ECMA-334, the C# language standard. There is also a nice explanation here: The C# Memory Model in Theory and Practice.

This kind of specification is not limited to “systems programming languages”. It’s important in any language that supports running code on >1 CPU at once, which no-GIL Python soon will be. CPUs make varying guarantees about instruction reordering and when writes to cache by one CPU will be visible to another, so if a language abstracts over OS threads at all in a way that allows shared memory, it should be well defined how it does so.

I’m a little surprised to see your stance on this. Of course evaluation order is well defined. We’re not talking about evaluation order per se. The issue is when and how side effects of evaluation in one thread are guaranteed to be visible to another thread. I skimmed the language reference a bit e.g Execution Order, but haven’t been able to find any specification of this behavior.

If it’s unspecified, then it is implementation and platform dependent, meaning I get to assume nothing unless I always control the hardware my code executes on.

1 Like

Then make a docs pull request. The fact that this is posted this in Python Help and not in Documentation means that the implicit a question about the current behavior and not about improving the documentation. The current behavior is very clear to anyone with experience in python and knowledge about how python is currently implemented. Up until now, spelling it out was pointless, and I am still of the opinion that it continues to be [1], but it probably wont be the worst clarification sentence in the documentation.


  1. But i am not going to continue to argue. The core point is that you are imaging optimizations that currently don’t exists. ↩︎

Make up your mind. Is it well defined, or does there need to be a statement added to the docs saying that code is executed in order?

Are you talking about evaluation order, or are you talking about something else?

And once again I ask: what in the documentation leads you to believe that this is NOT going to be the case? Where do you see something that says “by the way, the moment you import threading, the documented evaluation order ceases to be reliable”? I don’t see that anywhere, so I’m going to continue to expect that, threaded or not, evaluation order is as it is documented to be.

This find this disingenuous, given that:

  • It’s been discussed multiple times previously in the thread that what would be helpful is a clarification or addition of specification, implying documentation changes. The post category is not relevant to that concern.
  • CPython is one implementation of a language standard, not a specification unto itself. I can’t document a memory model for the language when the only reference is what CPython happens to do right now.

And that’s very nice for anyone with experience in Python and knowledge about how CPython is implemented. :upside_down_face:

Only a small handful of people are so utterly mistrustful of Python that they’re demanding a docs change. “Discussed multiple times” is meaningless when it’s the same people discussing it.