My Python program has multiple threads accessing the same Numpy array instance. Since Numpy releases the GIL, these threads can end up accessing the array simultaneously.
If the threads are concurrently accessing the same array element, this can clearly cause a race condition where the result depends on the specific order in which the threads happen to execute. However, in languages such as C++, concurrent conflicting memory access by multiple threads may cause a data race that results in completely undefined behaviour.
I would like to understand what semantics are guaranteed by Numpy in case of concurrent array access. Are there rules I must follow to ensure that my program has sequential consistency? What happens if I break those rules?
- If the threads simultaneously access the same array but never simultaneously access the same array element, is there any guarantee that this can not cause a data race?
- If one thread writes an array element that another thread is simultaneously reading, can this cause the write action to fail or the written data to become corrupted?
- Is there any guarantee that the consequences of concurrent conflicting array access will be limited to the contents of the array, or can it also lead to undefined behaviour in other parts of the program or maybe crash the Python interpreter?
- Do the answers to these questions depend on the underlying machine architecture, such as x86 vs arm?
I really hope to understand what the precise rules are in these cases.
I found a similar question, but the answer only confirms that the threads can cause conflicting access. No explanation of the semantics of Numpy in such cases:
Is python numpy array operation += thread safe?
Another similar question without answers:
Are ndarray assingments to different indexes threadsafe?
# Example of a program that performs simultaneous array access.
import threading
import numpy as np
a = np.zeros(100000, dtype=np.int16)
def countup():
for i in range(10000):
a[:] += 1
def countdown():
for i in range(10000):
a[:] -= 1
t1 = threading.Thread(target=countup)
t2 = threading.Thread(target=countdown)
t1.start()
t2.start()
t1.join()
t2.join()
# Some elements of the array will be non-zero.
print(np.amin(a), np.amax(a), np.sum(a != 0))