V
V
Vadym Tishchenko2021-08-03 15:41:45
Python
Vadym Tishchenko, 2021-08-03 15:41:45

Is the threading.Lock class necessary?

The question is this.
If Python has a GIL that blocks access of different threads to the same memory area, which is actually one of the performance anchors, then why do we need the threading.Lock class, because the threads are already locked by the GIL?

PS
It would probably be cool if someone had articles on this topic lying around

Answer the question

In order to leave comments, you need to log in

2 answer(s)
R
Roman Kitaev, 2021-08-03
@DieOld

Copy and run two different versions of the code.

Without 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()


With 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()


As you can see, the first option sometimes prints two lines on one line, and sometimes prints empty lines
More examples
Without 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()

With 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()

Well, a completely stubborn example for those who say that list is threadsafe (which is actually true, but logically not always) and you don’t need to use Lock:
Open

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()


And finally. Stop coding (or trying) on ​​threads. It's hard and no one needs it. For a long time there have been much more successful implementations of using all processor cores ( csp for example in golang). And if threads are used for IO (and in python they are 99.9% used for IO), then there is a rather usable asyncio for a long time.

K
kamenyuga, 2021-08-03
@kamenyuga

GIL which blocks access of different threads to the same memory area
No, the GIL guarantees that only one thread is running at a time. At the same time, every few tens/hundreds of processor cycles, running threads replace each other (if there is more than one).
which actually is one of the anchors in performance
A controversial and clumsy statement.
why is the threading.Lock class needed
To allow one thread to work, and to prohibit all others. Because otherwise, switching between threads will happen in general at a random moment in time, namely when the interpreter wants. He, of course, does not worry about the logic hardwired into the code and will not wait for the completion of any specific calculations / reading / writing.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question