Multithreading in Python | Set 2 (Synchronization)

Python Methods and Functions

This article discusses the concept of thread synchronization in the case of multithreading in the Python programming language.

Synchronization between threads

Thread synchronization is defined as a mechanism that ensures that two or more parallel threads do not simultaneously execute any particular program segment, known as a critical section .

Critical section refers to the parts of the program where the shared resource is accessed.

For example, in the diagram below, 3 threads are trying to simultaneously receive access to a shared resource or critical section. 

Concurrent access to a share may result in a race condition .

A race condition occurs when two or more threads can access shared data and they try to change it at the same time. As a result, the values ​​of variables may be unpredictable and vary depending on the timings of context switches of the processes.

Consider the program below to understand the concept of a race condition:

import threading

  
# global variable x

x = 0

 

def increment ():

  "" "

  function to increase the global variable x

“ »»

global x

x + = 1

 

def thread_task ():

"" "

  task for the stream

calls the function to increment 100,000 times.

  " ""

for _ in range ( 100000 ):

  increment ()

 

def main_task ():

global x

# setting global variable x to 0

x = 0

  

  # creating themes

t1 = threading.Thread (target = thread_task)

  t2 = threading.Thread (target = thread_task)

 

# start of topic

  t1.start ()

t2.start ()

 

# wait until streams finish their work

t1. join ()

t2.join ()

 

if __ name__ = = "__ main__" :

for i in range ( 10 ):

main_task ()

print ( " Iteration {0}: x = { 1} " . format (i, x))

Output:

 Iteration 0: x = 175005 Iteration 1: x = 200,000 Iteration 2: x = 200,000 Iteration 3: x = 169,432 Iteration 4: x = 153,316 Iteration 5: x = 200,000 Iteration 6: x = 167,322 Iteration 7: x = 200,000 Iteration 8: x = 169,917 Iteration 9 : x = 1 53589 

In the above program:

  • In the main_task function, two threads are created t1 and t2 , and the global variable x has the value 0.
  • Each thread has a target function thread_task, in which the function increments is called 100,000 times.
  • The increment function will increment the global variable x by 1 each time it is called.

The expected final value of x is 200,000, but what we get in 10 iterations of the main_task function is some other value.

This is due to simultaneous access of threads to the shared variable x . This unpredictability in x is nothing more than a race condition .

Below is a diagram that shows how a race condition in the above program:

Note that the expected value of x in the above diagram is 12, but due to a race condition it is 11!

Hence, we need a tool to synchronize correctly across multiple threads.

Using Locks

Module Threading provides a Lock class for dealing with race conditions. Locking is implemented using a semaphore provided by the operating system.

A semaphore is a synchronization object that controls access by multiple processes / threads to a common resource in a parallel programming environment. It is simply a value in a designated place in operating system (or kernel) storage that each process / thread can check and then change. Depending on the value that is found, the process / thread can use the resource or will find that it is already in use and must wait for some period before trying again. Semaphores can be binary (0 or 1) or can have additional values. Typically, a process / thread using semaphores checks the value and then, if it using the resource, changes the value to reflect this so that subsequent semaphore users will know to wait.

Class locks provides the following methods:

  • acquire ([lock]): to acquire a lock. Blocking can be blocking or non-blocking.
    • When called with the blocking argument True (the default), the thread will block until the lock is unlocked, then the lock is set to locked and returns True .
    • When called with a blocking argument set to False , the thread will not block execution. If the lock is unlocked, set it to locked and return True, otherwise, immediately return False .
  • release (): release the lock.
    • When the lock is locked, reset it to unlocked and revert it. If any other threads are blocked waiting to unlock the lock, allow only one of them to continue.
    • If the lock is already unlocked, a ThreadError is raised.

Consider the example below:

import threading

 
# global variable x

x = 0

 

def increment ():

"" "

  function to increment global variable x

  "" "

  global x

x + = 1

 

def thread_task (lock):

"" "

  task for the stream

  calls the increment function 100,000 times.

"" "

for _ in range ( 100000 ):

lock.acquire ()

  increment ()

lock. release ()

 

def main_task ():

global x

# setting global variable x to 0

x = 0

  

  # create blocking

lock = threading.Lock ()

 

  # creating themes

t1 = threading.Thread (target = thread_task, args = (lock,))

t2 = threading.Thread (target = thread_task, args = (lock,))

  

  # topic start

  t1.start ()

t2.start ()

 

# wait until threads finish

t1.join ()

  t2.join ()

 

if __ name__ = = "__ main__" :

for i in range ( 10 < / code> ):

main_task ()

print ( "Iteration {0}: x = {1}" . format (i, x))

Output:

 Iteration 0: x = 200000 Iteration 1: x = 200000 Iteration 2: x = 200000 Iteration 3: x = 200000 Iteration 4: x = 200000 Iteration 5: x = 200000 Iteration 6: x = 200000 Iteration 7: x = 200000 Iteration 8: x = 200000 Iteration 9: x = 200000 

Let's try to understand the above code step by step:

  • First, the object The Lock is created with:
     lock = threading.Lock () 
  • Then the lock is passed as an argument to the target function:
     t1 = threading.Thread (target = thread_task , args = (lock,)) t2 = threading.Thread (target = thread_task, args = (lock,)) 
  • In the critical section of the objective function, we apply locking using the method lock.acquire () . Once the lock is acquired, no other thread can access the critical section ( increment here) until the lock is released using the lock.release ( ) .
     lock.acquire () increment () lock.release () 

    As you can see from the results, the final x value is 200000 each time (which is the expected end result).

Below is a diagram showing the implementation of locks in the above program:

This brings us to the end of this series of tutorials on multithreading in Python
Finally, here are a few advantages and disadvantages of multithreading:

Advantages :

  • It does not block the user. This is because threads are independent of each other.
  • Better utilization of system resources is possible because threads execute tasks in parallel.
  • Improved performance on multiprocessor computers.
  • Multithreaded servers and interactive GUIs use multithreading exclusively.

Disadvantages :

  • As the number of threads increases, the complexity increases.
  • Synchronization of shared resources (objects, data) is necessary.
  • Difficult to debug, the result is sometimes unpredictable.
  • Potential deadlocks that lead to exhaustion, that is some threads may be out of service with poor design
  • Building and synchronizing threads is CPU and memory intensive.

This article is courtesy of Nikhil Kumar . If you are as Python.Engineering and would like to contribute, you can also write an article using contribute.python.engineering or by posting an article contribute @ python.engineering. See my article appearing on the Python.Engineering homepage and help other geeks.

Please post comments if you find anything wrong or if you'd like to share more information on the topic discussed above.