Python threading.Lock: защитить общий счётчик
threading.Lock защищает участок кода, который несколько потоков не должны выполнять одновременно. GIL не делает операции вроде counter += 1 атомарными: поток может быть вытеснен между чтением и записью. Блокировка вокруг общего состояния делает результат предсказуемым.
Синхронизировать инкремент
import threading
counter = 0
lock = threading.Lock()
def worker():
global counter
for _ in range(1000):
with lock:
counter += 1
threads = [threading.Thread(target=worker) for _ in range(4)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print(counter) # 4000Контекстный менеджер with lock захватывает блокировку и гарантированно освобождает её даже при исключении. Без него четыре потока могли бы прочитать одно и то же значение и потерять часть инкрементов. Чем короче критическая секция, тем меньше потоков ждут друг друга.
Понять повторный вход
import threading
lock = threading.RLock()
with lock:
with lock:
print("повторный вход") # повторный входRLock разрешает тому же потоку захватить блокировку повторно; освобождать её нужно столько же раз. Обычный Lock в такой ситуации зависнет навсегда. RLock полезен для рекурсивных функций и методов, которые вызывают друг друга, но не заменяет продуманную структуру блокировок.
Не удерживайте блокировку во время сетевого запроса, чтения большого файла или sleep: другие потоки будут простаивать. Не захватывайте несколько блокировок в разном порядке — это классическая причина взаимной блокировки. Если нужен таймаут, используйте lock.acquire(timeout=1) и обрабатывайте False.
Для процессов threading.Lock не работает: нужен multiprocessing.Lock или внешняя блокировка вроде файла/БД. Для потоковых данных лучше подходит queue.Queue, а для счётчика — itertools.count с осторожностью или отдельный сервис. Блокировка защищает только те операции, которые действительно выполняются под ней.
Документация: threading.Lock, RLock.