46

Let's say I am using a signal handler for handling an interval timer.

def _aHandler(signum, _):
  global SomeGlobalVariable
  SomeGlobalVariable=True

Can I set SomeGlobalVariable without worrying that, in an unlikely scenario that whilst setting SomeGlobalVariable (i.e. the Python VM was executing bytecode to set the variable), that the assignment within the signal handler will break something? (i.e. meta-stable state)

Update: I am specifically interested in the case where a "compound assignment" is made outside of the handler.

(maybe I am thinking too "low level" and this is all taken care of in Python... coming from an Embedded Systems background, I have these sorts of impulses from time to time)

Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
jldupont
  • 93,734
  • 56
  • 203
  • 318

4 Answers4

32

Simple assignment to simple variables is "atomic" AKA threadsafe (compound assignments such as += or assignments to items or attributes of objects need not be, but your example is a simple assignment to a simple, albeit global, variable, thus safe).

Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
  • but what about "compound assignment" outside of the handler? – jldupont Feb 18 '10 at 18:30
  • 2
    If the handler does (e.g.) `gvar = 3`, `gvar` is initially 7, and the code outside the handler does (e.g.) `gvar += 2`, then `gvar` could end up as being either 3, 5, or 9, depending on how the operations end up interleaved. That's technically "safe" (meaning, the process won't crash;-) but unlikely to be semantically OK. – Alex Martelli Feb 18 '10 at 22:07
  • 12
    Where is this specified? -1 for lack of authoritative reference. –  Nov 25 '15 at 20:46
  • 1
    @Elyse, there **is** no "authoritative reference" (I guess I could have quoted my own books, "authoritative" just by being best-sellers, but, how exactly would that help?-) -- so, in your world (what color is its sky?), would the world have been a better place if this question had been left unanswered, since NO "authoritative reference" exists?! My old friend and colleague the effbot had http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm -- but he now tags it as out of date, and, anyway, why's the effbot any more authoritative than the martellibot?-) How sad...! – Alex Martelli Nov 26 '15 at 00:25
  • 5
    Official documentation would count as authoritative, I'd say. If this is not documented, then it is implementation-dependent, no? – R. Martinho Fernandes Nov 26 '15 at 12:19
  • Maybe there's a test suite for all python implementations to run on, that tests this behaviour? – Ven Nov 26 '15 at 12:21
  • 7
    @R.MartinhoFernandes, definitely implementation dependent -- in the real world, of course, the CPython reference implementation is SO dominant that all others mostly try to stick to its (documented or not) behavior, making the issue one of only theoretic and pedantic relevance. Given this, I think that answering the question with "what actually happens" in the real world of the dominant implementation still has positive value compared to waffling or refusing to help, even if no "authoritative reference" exists (nor is likely to appear any time soon). – Alex Martelli Nov 27 '15 at 04:27
  • @Ven, no, no official test suite across implementations. Most implementations strive to pass CPython's implementation-specific suite -- but one can't reliably *test* atomicity of elementary operations anyway. – Alex Martelli Nov 27 '15 at 04:29
  • 2
    If the standard doesn't explicitly state that simple assignments are atomic, then the answer should mention that the behavior is up to the implementation. Right now the answer is making a strong claim without citing any authoritative source, so -1. – Richard Hansen Nov 30 '16 at 18:52
  • @RichardHansen There's no real language standard for python. While the official docs are taken seriously, they leave a lot of important things out, and it's nearly impossible to avoid making assumptions about the left-out parts (btw, to some extent it's true even for C++). So it's a lot more common in python to rely on CPython implementation details. That said, I do find it very unfortunate that the thread-safety is not discussed in the python docs; see https://bugs.python.org/issue15339. (Also, just over a decade ago, Guido expressed hope that thread-safety would be documented...) – max Dec 14 '19 at 00:32
17

Google's Style Guide advises against it

I'm not claiming that Google styleguides are the ultimate truth, but the rationale in the "Threading" section gives some insight (highlight is mine):

Do not rely on the atomicity of built-in types.

While Python’s built-in data types such as dictionaries appear to have atomic operations, there are corner cases where they aren’t atomic (e.g. if __hash__ or __eq__ are implemented as Python methods) and their atomicity should not be relied upon. Neither should you rely on atomic variable assignment (since this in turn depends on dictionaries).

Use the Queue module's Queue data type as the preferred way to communicate data between threads. Otherwise, use the threading module and its locking primitives. Learn about the proper use of condition variables so you can use threading.Condition instead of using lower-level locks.

So my interpretation is that in Python everything is dict-like and when you do a = b in the backend somewhere globals['a'] = b is happening, which is bad since dicts are not necessarily thread safe.

For a single variable, Queue is not ideal however since we want it to hold just one element, and I could not find a perfect pre-existing container in the stdlib that automatically synchronizes a .set() method. So for now I'm doing just:

import threading

myvar = 0
myvar_lock = threading.Lock()
with myvar_lock:
    myvar = 1
with myvar_lock:
    myvar = 2

It is interesting that Martelli does not seem to mind that Google style guide recommendation :-) (he works at Google)

I wonder if the CPython GIL has implications to this question: What is the global interpreter lock (GIL) in CPython?

This thread also suggests that CPython dicts are thread safe, including the following glossary quote that explicitly mentions it https://docs.python.org/3/glossary.html#term-global-interpreter-lock

This simplifies the CPython implementation by making the object model (including critical built-in types such as dict) implicitly safe against concurrent access.

Community
  • 1
  • 1
Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
  • 1
    I'm a little skeptical of the style guide's assertion that variable assignment might not be atomic since it relies on dictionaries. Dictionary operations are possibly not atomic when the keys are not builtin types, but for variable assignment the key is a string (a builtin type). – C S Jun 29 '19 at 21:22
  • @CS thanks for the feedback. While this is true for CPython, the main reason I would follow that advice is looking forward for other implementations that might not have the GIL. – Ciro Santilli OurBigBook.com Jun 30 '19 at 08:12
  • Looking at the last quote, can one conclude that any statement that generates a single bytecode instruction is thread safe ? – Tejas Kale May 22 '20 at 14:45
  • I'm particularly interested in the OP's original example: handling signals. How do you protect the main thread from a race condition if the Python signal handling function could be run at any time on the main thread. As far as I can see using locks and friends won't help here as we're in the same thread and would just dead-lock ourselves immediately. – Damian Birchler Dec 18 '20 at 14:40
16

you can try dis to see the underlying bytecode.

import dis

def foo():
    a = 1


dis.dis(foo)

produces the bytecode:

# a = 1
5             0 LOAD_CONST               1 (1)
              2 STORE_FAST               0 (a)

So the assignment is a single python bytecode (instruction 2), which is atomic in CPython since it executes one bytecode at a time.

whereas, adding one a += 1:

def foo():
    a = 1
    a += 1

produces the bytecode:

# a+=1
6             4 LOAD_FAST                0 (a)
              6 LOAD_CONST               1 (1)
              8 INPLACE_ADD
             10 STORE_FAST               0 (a)

+= corresponds to 4 instructions, which is not atomic.

Izana
  • 2,537
  • 27
  • 33
2

Compound assignment involves three steps: read-update-write. This is a race condition if another thread is run and writes a new value to the location after the read happens, but before the write. In this case a stale value is being updated and written back, which will clobber whatever new value was written by the other thread. In Python anything that involves the execution of a single byte code SHOULD be atomic, but compound assignment does not fit this criteria. Use a lock.

Eloff
  • 20,828
  • 17
  • 83
  • 112
  • In the situation depicted above, I have only a single thread of execution. Furthermore, it is not like I can "delay" the execution of the signal handler. Of course I can have recourse to a thread-safe queue if the opinion of the brain trust of SO prescribes so. – jldupont Feb 18 '10 at 18:45
  • If you have a single thread, where does the handler run? If it's on the same thread, then nothing can alter state while it's running in the first place. – Max Shawabkeh Feb 18 '10 at 18:53
  • @Max S. : you sure? Look at @Alex Martelli 's answer. – jldupont Feb 18 '10 at 19:01
  • jldupont, Pretty sure. Alex's answer is excellent, but it applies only when you have multiple threads of control; there's no race conditions when there's only one racer. – Max Shawabkeh Feb 18 '10 at 19:04
  • @Max S.: but in this case I believe we have another racer: the "signal dispatching agent".... I don't see any polling for signals. Of course, I might be totally out-for-lunch: I'd like to see the relevant documentation to support your case, please. – jldupont Feb 18 '10 at 19:09
  • @jldupont: Why on earth do you care about atomicity if you have only one thread? By definition you cannot have a race condition with only one thread/process. Forget compound assignment, every single function call is atomic in that case, no matter how much you do in the function, including your entry point. – Eloff Feb 19 '10 at 04:43
  • @jldupont you might want to read the first section of the [signal docs](http://docs.python.org/2/library/signal.html). Signals will run on the main thread, and furthermore "they can only occur between the “atomic” instructions of the Python interpreter". So simple assignment outside of the signal handler will not be affected, but compound assignment will. – num1 Oct 15 '13 at 23:07
  • What about the case of a signal interrupting an assignment from a long-running function call. For instance, instead of `True` in the handler listed in the original question, what if `SomeGlobalVariable` is set to `OtherGlobalVariable`, while `__main__` is running something like `OtherGlobalVariable=LongRunningFunc()`? Will OtherGlobalVariable ever have bad values, or will it only have either the old or the new value? The docs say "signals arriving during long calculations implemented purely in C ... may be delayed for an arbitrary amount of time." but don't say what happens in Python code. – Brian Minton Mar 03 '14 at 21:12
  • after some experimentation with `dis.dis`, I found that the function call is one atomic operation, and the assignment is another. Therefore, if a variable assignment from a function call is interrupted by a signal handler, the variable will only ever have the old or the new variable, never junk. – Brian Minton Mar 03 '14 at 21:35