I want to create a non-thread-safe chunk of code for experimentation, and those are the functions that 2 threads are going to call.
c = 0 def increment(): c += 1 def decrement(): c -= 1
Is this code thread safe?
If not, may I understand why it is not thread safe, and what kind of statements usually lead to non-thread-safe operations.
If it is thread-safe, how can I make it explicitly non-thread-safe?
Python is not by its self thread safe. But there are moves to change this: NoGil, etc. Removing the GIL does not make functions thread-safe.
Thread safety is a computer programming concept applicable to multi-threaded code. Thread-safe code only manipulates shared data structures in a manner that ensures that all threads behave properly and fulfill their design specifications without unintended interaction.
These operators are not thread-safe. Imagine two threads that increment a variable. If they do the operation serially, the variable ends up correctly incremented twice.
The C++ new and delete operators are thread safe, but this means that a thread may have to wait for a lock on these operations. Once memory is obtained for a thread, the thread_alloc memory allocator keeps that memory available for the thread so that it can be re-used without waiting for a lock.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With