How to use Python threading lock for shared resource?

I’m trying to understand the basics of threading and concurrency. I want a simple case where two threads repeatedly try to access one shared resource. Here’s the code I tried:

import threading

class Thread(threading.Thread):
    def __init__(self, t, *args):
        threading.Thread.__init__(self, target=t, args=args)
        self.start()

count = 0
lock = threading.Lock()

def increment():
    global count 
    lock.acquire()
    try:
        count += 1    
    finally:
        lock.release()
   
def bye():
    while True:
        increment()

def hello_there():
    while True:
        increment()

def main():    
    hello = Thread(hello_there)
    goodbye = Thread(bye)
    
    while True:
        print(count)

if __name__ == '__main__':
    main()

In this setup, I have two threads that try to increment the counter. I thought that when thread ‘A’ calls increment(), the lock would be acquired, preventing thread ‘B’ from accessing the counter until thread ‘A’ releases the lock.

However, when running the code, I still get inconsistent increments, which seems like a data race issue. How exactly is the Python threading lock supposed to work in this scenario?

Additionally, I’ve tried placing the locks inside the thread functions, but that doesn’t seem to resolve the issue.

Use with statement (Context Manager) for Lock:"

The most Pythonic way to acquire and release a lock is by using the with statement, which automatically handles the acquisition and release of the lock, reducing the chance of errors. Here’s how you can modify your code:

import threading

count = 0
lock = threading.Lock()

def increment():
    global count
    with lock:  # Automatically acquires and releases the lock
        count += 1

def bye():
    while True:
        increment()

def hello_there():
    while True:
        increment()

def main():
    hello = threading.Thread(target=hello_there)
    goodbye = threading.Thread(target=bye)
    
    hello.start()
    goodbye.start()
    
    while True:
        print(count)

if __name__ == '__main__':
    main()

By using with lock, the lock is acquired at the start of the block and released automatically when the block is exited, even if an error occurs. This ensures that only one thread increments the counter at a time.

Avoid Using acquire() and release() Manually:"

Alright, so here’s the thing—manual locking with acquire() and release() works, but it’s prone to human error. You might forget to release the lock, and then you’re stuck troubleshooting. The with statement makes life simpler by handling locking implicitly.

The code shared in Solution 1 demonstrates this approach. By wrapping your critical section in a with block, you make it both cleaner and safer. Less code, fewer bugs. Just let the context manager do the heavy lifting for you.

Ensure Threads are Started Before the Main Loop:"

One more thing to keep in mind: it’s always good practice to start your threads explicitly after creating them. While the start() calls inside a custom Thread class can work, being deliberate with your thread management is better. Here’s an example:

def main():
    hello = threading.Thread(target=hello_there)
    goodbye = threading.Thread(target=bye)
    
    hello.start()
    goodbye.start()
    
    while True:
        print(count)

if __name__ == '__main__':
    main()

By explicitly calling start() on each thread, you make sure both threads kick off their execution concurrently. Combine that with proper synchronization provided by the Python threading lock, and you’re golden.