Reputation: 94
I am almost new in Python threading. This is my code modeled on an example code found on GeeksForFeeks which explains threads behavior using lock. But the result - for me - is counterintuitive.
import threading
# global variable x
x = 0
# creating a lock
lock = threading.Lock()
def increment():
global x
x += 1
print("thread1:", x)
def decrement():
global x
x -= 1
print("thread2:", x)
def plus():
global lock
for _ in range(100000):
lock.acquire()
increment()
lock.release()
def minus():
global lock
for _ in range(100000):
lock.acquire()
decrement()
lock.release()
def main_task():
global x
# setting global variable x as 0
x = 0
# creating threads
t1 = threading.Thread(target=plus)
t2 = threading.Thread(target=minus)
# start threads
t1.start()
t2.start()
# wait until threads finish their job
t1.join()
t2.join()
if __name__ == "__main__":
main_task()
I would expect a result like this:
thread1: 1
thread2: 0
thread1: 1
thread2: 0
thread1: 1
thread2: 0
thread1: 1
...
but instead I obtain
thread1: 1
thread1: 2
thread1: 3
thread1: 4
thread1: 5
thread1: 6
thread1: 7
...
thread1: 151
thread2: 150
thread2: 149
thread2: 148
thread2: 147
thread2: 146
thread2: 145
thread2: 144
thread2: 143
thread2: 142
thread2: 141
...
Why does thread2 cannot acquire lock each time it is released from thread1?
What am I missing?
Upvotes: 1
Views: 427
Reputation: 32063
This happens for at least two reasons:
The GIL
is a single lock on the interpreter itself which adds a rule that execution of any Python bytecode requires acquiring the interpreter lock.
More general: The Lock
object is not quite right for you. Yes, it synchronizes access to the variable, but after the lock
is released and before the moment it is acquired, other code can perform several such iterations.
For interleaved synchronization, it is better to use the Condition
object, which allows the code to wait for other code to notify (essentially wake it up) that the first can execute:
# creating a lock
lock = threading.Condition()
# ...
def plus():
global lock
with lock:
for _ in range(100000):
increment()
lock.notify()
lock.wait()
lock.notify() # notify to finish companion thread
def minus():
global lock
with lock:
for _ in range(100000):
decrement()
lock.notify()
lock.wait()
lock.notify() # notify to finish companion thread
...
thread1: 1
thread2: 0
thread1: 1
thread2: 0
thread1: 1
thread2: 0
Upvotes: 2