Reputation: 175
i'm currently trying to unterstand threading in python and i wrote a program that ideally would have 2 threads alternating between incrementing and decrementing a global variable but no matter how i spread out the lock it inevitably becomes out of sync.
number = 0
lock = threading.Lock()
def func1():
global number
global lock
while True:
try:
lock.acquire()
number += 1
finally:
lock.release()
print(f"number 1 is: {number}")
time.sleep(0.1)
def func2():
global number
global lock
while True:
try:
lock.acquire()
number -= 1
finally:
lock.release()
print(f"number 2 is: {number}")
time.sleep(0.1)
t1 = threading.Thread(target=func1)
t1.start()
t2 = threading.Thread(target=func2)
t2.start()
t1.join()
t2.join()
the output should look something like this:
number 1 is: 1
number 2 is: 0
number 1 is: 1
number 2 is: 0
number 1 is: 1
number 2 is: 0
number 1 is: 1
number 2 is: 0
but right now it looks like this:
number 1 is: 1
number 2 is: 0
number 1 is: 1
number 2 is: 0
number 2 is: -1number 1 is: 0
number 2 is: -1number 1 is: 0
number 1 is: 1number 2 is: 0
any idea how to do this without falling out of sync?
Upvotes: 1
Views: 2609
Reputation: 175
thanks for all your answers, i remember seing someone in the comments mentioned using events or something like that and that solved the issue. here's the code:
number = 0
event_number = threading.Event()
event_number.clear()
def func1():
global number
global event_number
while True:
if not event_number.is_set():
number += 1
print(f"func 1 is {number}")
event_number.set()
else:
pass
time.sleep(2)
def func2():
global number
global event_number
while True:
if event_number.is_set():
number -= 1
print(f"func 2 is {number}")
event_number.clear()
else:
pass
time.sleep(2)
t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)
t1.start()
t2.start()
t1.join()
t2.join()
now i notice that sometimes one of the loops will either not wait it's alloted time and print right away or wait double the time but at least the number only stays within those 2 values.
Upvotes: 1
Reputation:
For starters, time.sleep
is not exactly accurate. And depending on the python-implementation you're using (most likely cpython) multithreading might not quite work the way you're expecting it to. These two factors allow the initially correct timing of your threads to get out of sync within fairly short time.
There solution for this problem is to enforce alternate operation on the variable by the two threads via two locks:
import time
import threading
var = 0
def runner(op, waitfor, release):
global var
while True:
try:
# wait for resource to free up
waitfor.acquire()
# operation
var = op(var)
print(f"var={var}")
finally:
# notify other thread
release.release()
time.sleep(0.1)
# init locks for thread-synchronization
lock_a = threading.Lock()
lock_b = threading.Lock()
lock_a.acquire()
lock_b.acquire()
# create and start threads (they'll wait for their lock to be freed)
thread_a = threading.Thread(target=runner, args=(lambda v: v - 1, lock_a, lock_b))
thread_b = threading.Thread(target=runner, args=(lambda v: v + 1, lock_b, lock_a))
thread_a.start()
thread_b.start()
# let thread_b start the first operation by releasing the lock
lock_b.release()
In the above code, each thread has a lock that can be used to notify it, that the resource may be used by it. Thus threads can hand control over the global variable to each other.
Upvotes: 0
Reputation: 1287
First, avoid using global variables with threads in python. Use a queue to share the variables instead.
Second, the lock acquisition in non-deterministic. At the moment a lock is released, you have no guarantee that the other thread will grab it. There is always a certain probability that the thread that just released the lock can grab it again before the other thread.
But in your case, you can avoid problems because you know the state that the variable needs to be to accept modifications by one thread or the other. So, you can enforce the protection for modification by verifying if the variable is in the right state to accept a modification.
Something like:
from threading import Thread
import time
from queue import Queue
def func1(threadname, q):
while True:
number = q.get()
if number == 0:
number += 1
print(f"number 1 is: {number}")
q.put(number)
time.sleep(0.1)
def func2(threadname, q):
while True:
number = q.get()
if number == 1:
number -= 1
print(f"number 2 is: {number}")
q.put(number)
time.sleep(0.1)
queue = Queue()
queue.put(0)
t1 = Thread(target=func1, args=("Thread-1", queue))
t2 = Thread(target=func2, args=("Thread-2", queue))
t1.start()
t2.start()
t1.join()
t2.join()
Upvotes: 2