2

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?

3
  • FYI, you don't need global lock here. global is only needed when you are assigning a new value to the name. And they're never going to alternate perfectly, because you can't predict how long each thread will get until it has to release the CPU. Commented Aug 18, 2022 at 22:22
  • In practice, neither thread will sleep for exactly 0.1 seconds, so they'll eventually drift. It's not realistic to expect them to perfectly alternate forever. Commented Aug 18, 2022 at 22:30
  • 1
    Separate advice: multi-threaded communication is often simpler when done purely through messaging and ADTs like queue. Commented Aug 18, 2022 at 22:33

3 Answers 3

2

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()
Sign up to request clarification or add additional context in comments.

14 Comments

Polling is a bad idea. Especially when primitives for proper synchronization are easily available.
@Paul I think it depends on the use case. If you know the state that the variable needs to be to accept modifications by one thread or the other (like in this case) it can be an optimization letting it be accessible randomly by the two threads.
Keep in mind that your threads will quite regularly wake up just to find out that they should've remained asleep and go back to sleep. Also we're dealing with python here, so this is most likely not a considerable optimization anyways. And actually this code works correctly purely by chance - though with a greater likelihood than OPs code. There's nothing stopping the threads of interlacing their operations in such a way that a "wrong" result is printed.
@Paul I think it depends if the total time they spent waking up to find out they should've remained asleep is greater than the total time taken by the lock operations. We need to measure it to be sure. Also, I don't get why you say this works by chance. At what point can it go wrong exactly ?
sorry, my bad. Actually the code works fine. I missed the fine detail that pythons queue-implementation is synchronized. This basically makes your entire argument void though, because you're actually using locks anyways. They're simply hidden away in your queue instead of being openly visible. And you can still encounter the variable in an invalid state. So you're definitely worse off than simply using a lock.
|
1

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.

Comments

0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.