Is the += operator thread-safe in Python?

No, this code is absolutely, demonstrably not threadsafe.

import threading

i = 0

def test():
    global i
    for x in range(100000):
        i += 1

threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
    t.start()

for t in threads:
    t.join()

assert i == 1000000, i

fails consistently.

i += 1 resolves to four opcodes: load i, load 1, add the two, and store it back to i. The Python interpreter switches active threads (by releasing the GIL from one thread so another thread can have it) every 100 opcodes. (Both of these are implementation details.) The race condition occurs when the 100-opcode preemption happens between loading and storing, allowing another thread to start incrementing the counter. When it gets back to the suspended thread, it continues with the old value of "i" and undoes the increments run by other threads in the meantime.

Making it threadsafe is straightforward; add a lock:

#!/usr/bin/python
import threading
i = 0
i_lock = threading.Lock()

def test():
    global i
    i_lock.acquire()
    try:
        for x in range(100000):
            i += 1
    finally:
        i_lock.release()

threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
    t.start()

for t in threads:
    t.join()

assert i == 1000000, i

(note: you would need global c in each function to make your code work.)

Is this code thread safe?

No. Only a single bytecode instruction is ‘atomic’ in CPython, and a += may not result in a single opcode, even when the values involved are simple integers:

>>> c= 0
>>> def inc():
...     global c
...     c+= 1

>>> import dis
>>> dis.dis(inc)

  3           0 LOAD_GLOBAL              0 (c)
              3 LOAD_CONST               1 (1)
              6 INPLACE_ADD         
              7 STORE_GLOBAL             0 (c)
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE        

So one thread could get to index 6 with c and 1 loaded, give up the GIL and let another thread in, which executes an inc and sleeps, returning the GIL to the first thread, which now has the wrong value.

In any case, what's atomic is an implementation detail which you shouldn't rely on. Bytecodes may change in future versions of CPython, and the results will be totally different in other implementations of Python that do not rely on a GIL. If you need thread safety, you need a locking mechanism.


To be sure I recommend to use a lock:

import threading

class ThreadSafeCounter():
    def __init__(self):
        self.lock = threading.Lock()
        self.counter=0

    def increment(self):
        with self.lock:
            self.counter+=1


    def decrement(self):
        with self.lock:
            self.counter-=1

The synchronized decorator can also help to keep the code easy to read.