@impakashi

Для чего нужен lock в python? Как работает данный пример кода?

Нашёл пример кода, но даже с закомментированными блокировками код адекватно отрабатывает. Как это объяснить?
import threading

protected_resource = 0

NUM = 50000
mutex = threading.Lock()

# Потокобезопасный инкремент
def safe_plus():
    global protected_resource
    for i in range(NUM):
        # Ставим блокировку
        # mutex.acquire()
        protected_resource += 2
        # mutex.release()
    print(protected_resource)

# Потокобезопасный декремент
def safe_minus():
    global protected_resource
    for i in range(NUM):
        # mutex.acquire()
        protected_resource -= 1
        # mutex.release()
    print(protected_resource)

        
thread1 = threading.Thread(target = safe_plus)
thread2 = threading.Thread(target = safe_minus)

thread1.start()
thread2.start()
thread1.join()
thread2.join()

print ("Результат без блокировки %s" % protected_resource)
  • Вопрос задан
  • 287 просмотров
Решения вопроса 1
Vindicar
@Vindicar
RTFM!
Это объясняется тем, что в базовом питоне потоки не вполне честные - они конкурируют за global interpreter lock, так что код выполняется всё равно поочерёдно. Так что многопоточность в питоне полезна с точки зрения распараллеливания, но не ускорения. ЕМНИП, есть реализации питона, в которых нет этой GIL problem.
Но нужно иметь ввиду, что этот GIL блокирует только элементарные операции (как в твоём примере), тогда как явное использование lock может накрывать целые блоки кода, состоящие из нескольких операций с защищаемым ресурсом.

Вот тебе пример:
import threading
import time

class Data:
    def __init__(self):
        self.x: int = 0
        self.y: int = 0


do_sleep = False
run = True


def reader(d: Data):
    while run:
        x, y = d.x, d.y
        # по идее это условие не должно выполниться никогда
        if (x != 0) != (y != 0):  
            print(f'Got x={x} and y={y}')
        else:
            print(f'OK {x}', end='\x08\x08\x08\x08')


def writer(d: Data):
    while run:
        if d.x == 0:
            d.x = 1
            if do_sleep: pass
            d.y = 1
        else:
            d.x = 0
            if do_sleep: pass
            d.y = 0


do_sleep = False
instance = Data()
reader_thread = threading.Thread(target=reader, args=(instance,), daemon=True)
writer_thread = threading.Thread(target=writer, args=(instance,), daemon=True)
reader_thread.start()
writer_thread.start()
try:
    input()
finally:
    run = False
    reader_thread.join()
    writer_thread.join()


На моей машине, если if do_sleep: pass закомментировать, то в консоли высвечивается только OK - иными словами, присваивание двух полей выполняется достаточно быстро, чтобы поток не успел переключиться в промежутке. Как следствие, reader() всегда видит либо x=0 y=0, либо x=1 y=1.
Но если if do_sleep: pass оставить, то выполнение тела цикла замедляется достаточно, чтобы поток успел переключиться - и, как следствие, reader() начинает видеть структуру данных Data в неконсистентном состоянии, когда x=0 y=1 или когда x=1 y=0.
И вот чтобы не гадать "успеет - не успеет", нужно в таких случаях защищать связные серии обращений к структуре с помощью мьютекса, ну или в питоновских терминах - Lock.
Ответ написан
Комментировать
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы
22 нояб. 2024, в 12:20
10000 руб./за проект
22 нояб. 2024, в 11:53
3000 руб./за проект
22 нояб. 2024, в 11:51
20000 руб./за проект