aleio1
aleio1

Reputation: 94

How to execute threads in an interleaved way in Python

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

Answers (1)

alex_noname
alex_noname

Reputation: 32063

This happens for at least two reasons:

  1. 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.

  2. 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

Related Questions