Нужен ли класс threading.Lock?

Вопрос вот в чем.
Если у питона есть GIL который блокирует доступ разных потоков к одному и тому же участку памяти, что собственно является одним из якорей в производетельности, то зачем нужен класс threading.Lock, ведь потоки уже заблокированы GIL-ом?

P.S.
Наверное было бы круто, если у кого-то завалялись статьи на эту тему
  • Вопрос задан
  • 912 просмотров
Решения вопроса 2
sergey-gornostaev
@sergey-gornostaev Куратор тега Python
Седой и строгий
GIL гарантирует только, что два потока не работают параллельно, и нужен для простого управления памятью на основе счётчика ссылок и простой интероперабельности, на уровне прикладного кода никаких гарантий он вам не даёт, два потока вполне могут конкурентно работать с одними данными и устроить гонку.
Ответ написан
Комментировать
@deliro
Скопируй и запусти две разных версии кода.

Без Lock

from threading import *

def work(i):
    for _ in range(100):
        print(f"hello i'm a thread #{i}")

t1 = Thread(target=work, args=(1,))
t2 = Thread(target=work, args=(2,))
t1.start()
t2.start()
t1.join()
t2.join()



С Lock

from threading import *

lock = Lock()

def work(i):
    for _ in range(100):
        with lock:
            print(f"hello i'm a thread #{i}")

t1 = Thread(target=work, args=(1,))
t2 = Thread(target=work, args=(2,))
t1.start()
t2.start()
t1.join()
t2.join()



Как можешь заметить, первый вариант иногда печатает две строки на одной, а иногда печатает пустые строки

Ещё примеры

Без Lock

from threading import *
from time import sleep


class GlobalState:
    def __init__(self, x):
        self.x = x
        
    def set_x(self, x):
        self.x = x

def reader(state: GlobalState):
    if state.x % 2 == 0:
        sleep(0.01)  # simulate OS context switch
        print(f"{state.x=} is even")
    else:
        print(f"{state.x=} is odd")
        

def changer(state: GlobalState):
    state.set_x(state.x + 1)

state = GlobalState(2)
t1 = Thread(target=reader, args=(state,))
t2 = Thread(target=changer, args=(state,))
t1.start()
t2.start()
t1.join()
t2.join()


С Lock

from threading import *
from time import sleep


class GlobalState:
    def __init__(self, x):
        self.x = x
        self.lock = Lock()
        
    def set_x(self, x):
        self.x = x

def reader(state: GlobalState):
    with state.lock:
        if state.x % 2 == 0:
            sleep(0.01)  # simulate OS context switch
            print(f"{state.x=} is even")
        else:
            print(f"{state.x=} is odd")
        

def changer(state: GlobalState):
    with state.lock:
        state.set_x(state.x + 1)

state = GlobalState(2)
t1 = Thread(target=reader, args=(state,))
t2 = Thread(target=changer, args=(state,))
t1.start()
t2.start()
t1.join()
t2.join()


Ну и совсем упоротый пример для тех, кто говорит, что list — threadsafe (что фактически является истиной, но логически не всегда) и не нужно использовать Lock:
Открыть

from threading import *
from random import *

class GlobalState:
    def __init__(self):
        self.x = []
        
    def do_something_changing(self):
        if random() < 0.5:
            self.x.append(1)
        elif self.x:
            self.x.pop()

def reader(state: GlobalState):
    for _ in range(10000000):
        if len(state.x) % 2 == 0:
            if len(state.x) % 2 != 0:  # wtf how it's possible?
                print(f"length {len(state.x)} was even before context switch")

def changer(state: GlobalState):
    for _ in range(10000000):
        state.do_something_changing()

state = GlobalState()
t1 = Thread(target=reader, args=(state,))
t2 = Thread(target=changer, args=(state,))
t1.start()
t2.start()
t1.join()
t2.join()



И напоследок. Хватит программировать (или пытаться) на тредах. Это сложно и никому не нужно. Давно существуют куда более удачные реализации использования всех ядер процессора (csp например в golang). А если треды используются для IO (а в питоне они в 99.9% используются именно для IO), то давно есть и довольно юзабельный asyncio.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
@kamenyuga
GIL который блокирует доступ разных потоков к одному и тому же участку памяти
Нет, GIL гарантирует, что в каждый момент времени работает только один поток. При этом каждые несколько десятков/сотен тактов процессора работающие потоки сменяют друг друга (если их больше одного).

что собственно является одним из якорей в производетельности
Спорное и корявое утверждение.

зачем нужен класс threading.Lock
Чтобы дать работать одному потоку, а всем остальным запретить. Потому что иначе переключение между потоками произойдет в общем-то в случайный момент времени, а именно тогда, когда захочет интерпретатор. Он, конечно же, не парится по поводу зашитой в код логики и не будет ждать завершения каких-то конкретных вычислений/чтения/записи.
Ответ написан
Комментировать
Ваш ответ на вопрос

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

Похожие вопросы