[HN Gopher] Data races in Python, despite the Global Interpreter... ___________________________________________________________________ Data races in Python, despite the Global Interpreter Lock Author : verdagon Score : 24 points Date : 2022-02-21 21:02 UTC (1 hours ago) (HTM) web link (verdagon.dev) (TXT) w3m dump (verdagon.dev) | _ache_ wrote: | I'm unable to reproduce. Increasing the number of iterations to | `1000000` and using a temporary variable `c = counter + 1; | counter = c` doesn't help either. | | Why ? I'm on Linux, using Python 3.10. It's only happening using | Python2.7. | Aperocky wrote: | Python 2.x is unsupported and anyone using it should operate on | the assumption that whatever bug its got is final unless | they're the one who is going to fix it. | verdagon wrote: | Author here, u/skeeto from | https://www.reddit.com/r/programming/comments/sxy5q4/pythons... | has some good insight into the 3.10 difference: | | > Ironically, CPython 3.10 has gone the opposite direction and | made thread scheduling much more deterministic. It now only | releases the GIL on backwards edges in the byte code The | example in the article always prints 40000000 in CPython 3.10! | I expect this will ultimately make Python code less reliable in | the future as many programs will accidentally depend on this | behavior. | dehrmann wrote: | And there's this: | https://web.archive.org/web/20201108091210/http://effbot.org... | samwillis wrote: | Interestingly if you change the line to `counter += 1` it still | has the race condition. I'm not sure how the byte code is | different for the two options but it doesn't make a difference, I | had hoped it would. | miohtama wrote: | In the Python VM there is no atomic increment bytecode. So | `counter += 1` should be exactly same as `counter = counter + | 1`. | | Here is an example what thread safe increment looks like in | Python: | | https://julien.danjou.info/atomic-lock-free-counters-in-pyth... | | You need to lock it explicitly. | | Note that `INC` instructor for x86 architecture needs explicit | hints/locks as well, so this should suprise anyone: | | https://stackoverflow.com/q/10109679 | jasonhansel wrote: | In general, "+=" probably needs to be non-atomic to support | "__add__" overloading; Python wouldn't be able to call | arbitrary "__add__" methods in a way that could guarantee | atomicity. | [deleted] | tomp wrote: | Python's GIL does exactly nothing to prevent data races (or any | other concurrency issues); it merely protects the _runtime_ from | memory corruption stemming from concurrency. | | Obviously, the fundamental issue with concurrency is programmer's | _intent_. This statement: x += 1; y -= 1 | | can be interpreted in two ways: atomic { | x += 1 y -= 1 } | | or atomic { x += 1 } atomic { y -= 1 } | | The best the compiler can do would be to alert the programmer of | the ambiguity; I know of no compiler that does that. | dzqhz wrote: | I don't see how it could ever be interpreted the first way. | verdagon wrote: | The article isn't trying to claim either interpretation, it's | just using it as an example to show that the GIL doesn't | actually help protect users from concurrency problems. You'd | be surprised how many people think that! | | I used to think that it did as well, but then my C/Java brain | kicked in and realized that couldn't be correct. I wrote this | article to help others see it in action. | nyanpasu64 wrote: | If only one thread is reading or writing the counter at a time, | and holds the GIL while doing so, it's not a data race, but a | mutex which fails to ensure the atomicity of the read-modify- | write operation: with GIL: x = counter + | 1 with GIL: counter = x | jondgoodwin wrote: | You claim that a language can guarantee completely deterministic | runs. How is that possible in Vale? | verdagon wrote: | It's tricky but it is possible, if we: | | 1. Don't allow any undefined behavior or `unsafe` code in the | language. | | 2. Record all inputs from FFI. | | 3. Carefully track the orderings of interactions across | threads. | | The article goes into the first two, but the third one is the | most interesting IMO: | | When we unlock a mutex or send a message, we assign a "sequence | number" (similar to what we see in TCP packets). | | Whenever we lock a mutex or receive a message, we read the | sequence number and record it to this thread's "recording". | | When replaying, we use that sequence number and that file to | make sure we're reading in the same order as the previous | execution. | bb88 wrote: | Interesting, but how do you know if you've captured all the | potential states for all possible inputs to a program? | | An unexpected state would seem to break the memory model, and | lead to corrupted data, wouldn't it? | jasonhansel wrote: | That's a logical race, not a data race, right? Technically, | "counter = counter + 1" accesses counter twice (once to read and | once to write). | | The fact that "counter" gets modified between the read and the | write doesn't imply that multiple accesses were happening | simultaneously; it just means that accesses from different | threads were getting interleaved. The GIL would only prevent the | former but not the latter, since a thread can give up the GIL at | any point between operations. | bb88 wrote: | The GIL is for internal python state. Not for atomic preservation | of python data types. | | I learned very early on that python threads should be treated | like C threads, and therefore should be avoided. Also there | really isn't a performance gain to using threads (other than | maybe waiting for IO completion). ___________________________________________________________________ (page generated 2022-02-21 23:00 UTC)