Нет, этот код абсолютно, очевидно, не потокобезопасен.
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
последовательно терпит неудачу.
i + = 1 преобразуется в четыре кода операции: загрузить i, загрузить 1, добавить два и сохранить его обратно в i. Интерпретатор Python переключает активные потоки (выпуская GIL из одного потока, чтобы другой поток мог его иметь) через каждые 100 кодов операций. (Оба они являются деталями реализации.) Условие состязания возникает, когда между загрузкой и сохранением происходит прерывание по 100 опкодам, что позволяет другому потоку начать увеличивать счетчик. Когда он возвращается к приостановленному потоку, он продолжает со старым значением «i» и отменяет приращения, выполняемые другими потоками за это время.
Сделать его потокобезопасным просто; добавить блокировку:
#!/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