105

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.

The code:

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

So, I have two threads, both trying to increment the counter. I thought that if thread 'A' called increment(), the lock would be established, preventing 'B' from accessing until 'A' has released.

Running the makes it clear that this is not the case. You get all of the random data race-ish increments.

How exactly is the lock object used?

Additionally, I've tried putting the locks inside of the thread functions, but still no luck.

Neuron
  • 5,141
  • 5
  • 38
  • 59
Zack
  • 4,177
  • 8
  • 34
  • 52
  • @Ignacio Vazquez-Abrams - Should now. I left out the `if __name__` bit. Is that what you were referring to? – Zack May 09 '12 at 22:51
  • 1
    It doesn't run for me either. I would expect your thread creation to look like: `hello = threading.Thread(target=hello_there)` and then for the thread to be started `hello.start()`. – Steven Rumbalski May 09 '12 at 22:53
  • 17
    Did you know that you can use `with lock:` rather than `lock.acquire(); try: ...; finally: lock.release()`? – Chris Morgan May 09 '12 at 22:54
  • Also, I don't really understand your question. What is the code trying to demonstrate? – Steven Rumbalski May 09 '12 at 22:55
  • @Steven Rumbalski -- heh.. OK Works now! I forgot I subclassed threading to look more like it does in the book. My question is, how is `lock` used to prevent multiple threads from accessing a shared resource concurrently. I only want one thread to be able to access the variable count at any given time. Is that not what `lock` is used for? – Zack May 09 '12 at 23:02
  • 2
    You are using lock correctly. What makes you think you are not? – jdi May 09 '12 at 23:05
  • @Zack You need to look into the GIL (global interpreter lock). There is no parallelism in execution, only threads blocked waiting for I/O can be preempted and have other python threads execute. If you want parallelism, you need to use multiple processes. – doug65536 Dec 08 '21 at 06:30
  • I am confused. Isn't it true that Python interpreter uses GIL to make sure a python object is not shared or updated by two thread simultaneously. Then why do we need to apply this lock feature exclusively in our script? – KnowledgeSeeeker Dec 14 '21 at 18:44

2 Answers2

140

You can see that your locks are pretty much working as you are using them, if you slow down the process and make them block a bit more. You had the right idea, where you surround critical pieces of code with the lock. Here is a small adjustment to your example to show you how each waits on the other to release the lock.

import threading
import time
import inspect

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 incre():
    global count
    caller = inspect.getouterframes(inspect.currentframe())[1][3]
    print "Inside %s()" % caller
    print "Acquiring lock"
    with lock:
        print "Lock Acquired"
        count += 1  
        time.sleep(2)  

def bye():
    while count < 5:
        incre()

def hello_there():
    while count < 5:
        incre()

def main():    
    hello = Thread(hello_there)
    goodbye = Thread(bye)


if __name__ == '__main__':
    main()

Sample output:

...
Inside hello_there()
Acquiring lock
Lock Acquired
Inside bye()
Acquiring lock
Lock Acquired
...
jdi
  • 90,542
  • 19
  • 167
  • 203
  • 2
    Oh, neat! I guess I was getting confused by the `print count` in `main()` showing erratic iteration. But I guess it kind of makes being that I had three loops running concurrently. Thanks! Also TIL about the inspect module. Very cool. – Zack May 09 '12 at 23:26
  • @Zack: Ya I assume it was that main print confusing you. It had no limit on it so it was printing faster than the threads were even changing it. – jdi May 09 '12 at 23:31
  • @jdi I'm trying to understand locking, so I added a print(count) to your incre function right after the count += 1, but when I run the code it gets to 6? – ErinGoBragh Mar 21 '16 at 18:27
  • @ErinGoBragh that is because we have two threads trying to call incre() while the count is less than 5. One gets the lock and count becomes 5.Then the lock is released and the other thread gets through and the count becomes 6. If it was very important to keep the count at 5 or less, you would need to check the count once you have acquired the lock and not do anything if it's 5 – jdi Mar 21 '16 at 19:11
  • So with lock waits for it to be available? – ErinGoBragh Mar 21 '16 at 19:18
  • Any number of threads can try to acquire the lock. One will acquire it and the rest will block. Once the lock is released, another thread will wake up. What you are seeing is that both threads pass the value check, while the counter is less than 5, and both go for the lock. One gets through and the other sleeps. The counter increments, and then the other thread wakes up and also increments the counter. – jdi Mar 22 '16 at 01:09
  • I think this would be clearer if you explicitly use .acquire() and .release() – user1893354 Mar 27 '19 at 04:12
  • @user1893354, why is that? Do you mean from a teaching perspective or from a production solution perspective? I feel it is safer to use a context which ensures the lock will be freed if the code decides to return early or raises an exception. – jdi Mar 28 '19 at 05:05
  • @jdi Teaching, since that is what the code in the original question used – user1893354 Mar 28 '19 at 22:42
  • Thanks @jdi for this crystal clear explanation ! – Ragib Huda Apr 22 '21 at 07:41
-14
import threading 

# global variable x 
x = 0

def increment(): 
    """ 
    function to increment global variable x 
    """
    global x 
    x += 1

def thread_task(): 
    """ 
    task for thread 
    calls increment function 100000 times. 
    """
    for _ in range(100000): 
        increment() 

def main_task(): 
    global x 
    # setting global variable x as 0 
    x = 0

    # creating threads 
    t1 = threading.Thread(target=thread_task) 
    t2 = threading.Thread(target=thread_task) 

    # start threads 
    t1.start() 
    t2.start() 

    # wait until threads finish their job 
    t1.join() 
    t2.join() 

if __name__ == "__main__": 
    for i in range(10): 
        main_task() 
        print("Iteration {0}: x = {1}".format(i,x))
O. Aroesti
  • 1,016
  • 11
  • 32
  • 17
    Please don't post only code as answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes. – Mark Rotteveel Nov 04 '20 at 14:04