First of all, what are critical sections in Python? Well, they’re basically those parts of your code that need to be executed sequentially by only one thread at a time. In other words, if two threads try to access the same critical section simultaneously, one of them will have to wait until the first one is done before continuing.
Now, you might ask yourself why we even need this in Python since it’s an interpreted language and not compiled like C or Java. The answer lies in concurrency a technique that allows us to run multiple tasks at once by breaking down our code into smaller pieces that can be executed independently. However, if we don’t handle these tasks properly, they might interfere with each other and cause unexpected behavior or even crashes.
So how do critical sections help us prevent this? By providing a way to synchronize access to shared resources like variables, files, or databases. This ensures that our code is thread-safe and doesn’t produce any race conditions (when two threads try to modify the same variable at the same time).
But wait, you might say isn’t Python already thread-safe by default? Well, sort of… but not really. While it’s true that most built-in data structures in Python are thread-safe, this doesn’t mean we can ignore critical sections altogether. In fact, there are many cases where we need to use them explicitly to avoid any potential issues.
For example, let’s say you have a simple counter variable that needs to be incremented by multiple threads:
# Define a global variable "counter" and initialize it to 0
counter = 0
# Define a function "increment" that will increment the global variable "counter" by 1
def increment():
# Use the "global" keyword to access and modify the global variable "counter" within the function
global counter
counter += 1
# Start two threads and run the "increment" function in each one
# This will result in the counter being incremented twice, once in each thread
threading.Thread(target=increment).start()
threading.Thread(target=increment).start()
At first glance, this code might seem fine we’re using a global variable to keep track of our counter and incrementing it by two threads simultaneously. However, if you run this program for long enough (or with high concurrency), you might notice some unexpected results:
# Import necessary libraries
import threading # Importing the threading library to create and manage threads
import time # Importing the time library to add a delay in the program
# Initialize global variable
counter = 0 # Initializing the global variable 'counter' to keep track of the count
# Define function to increment counter
def increment():
global counter # Using the 'global' keyword to access and modify the global variable 'counter'
counter += 1 # Incrementing the value of 'counter' by 1
# Create two threads
t1 = threading.Thread(target=increment) # Creating a thread with the target function as 'increment'
t2 = threading.Thread(target=increment) # Creating another thread with the same target function as 'increment'
# Start the threads
t1.start() # Starting the first thread
t2.start() # Starting the second thread
# Add a delay to see the results
time.sleep(0.5) # Adding a delay of 0.5 seconds to allow the threads to finish their execution
# Print the final value of counter
print("Counter:", counter) # Printing the final value of 'counter' after the threads have finished their execution
# Output:
# Counter: 2 # The expected output is 2, as both threads increment the value of 'counter' by 1, resulting in a final value of 2.
In this example, we’re using `time.sleep()` to pause our program and let the threads run for half a second before printing out the final value of our counter variable. However, if you run this code multiple times, you might get different results each time sometimes it will print 4 or 6, but other times it might print 2 or even 0!
This is because we’re not using critical sections to synchronize access to the `counter` variable. As a result, two threads might try to increment it at the same time and produce unexpected results (either by adding both values together or overwriting each other). To fix this issue, we can use Python’s built-in locking mechanism:
# Import necessary libraries
import threading # Import the threading library to create and manage threads
import time # Import the time library to add a delay
# Create a lock object to synchronize access to the counter variable
lock = threading.Lock()
# Initialize the counter variable
counter = 0
# Define a function to increment the counter
def increment():
global counter # Use the global keyword to access the global variable
with lock: # Use the lock object to ensure only one thread can access the critical section at a time
counter += 1 # Increment the counter by 1
# Create two threads and assign the increment function as the target
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
# Start the threads
t1.start()
t2.start()
# Add a delay to allow the threads to finish their execution
time.sleep(0.5)
# Print the final value of the counter
print("Counter:", counter)
# Output: Counter: 2
# Explanation:
# The script starts by importing the necessary libraries, threading and time.
# A lock object is created using the threading library to synchronize access to the counter variable.
# The counter variable is initialized to 0.
# The increment function is defined, which uses the global keyword to access the global variable and increments it by 1 within a critical section, using the lock object to ensure only one thread can access it at a time.
# Two threads are created and assigned the increment function as the target.
# The threads are started and a delay of 0.5 seconds is added to allow them to finish their execution.
# The final value of the counter is printed, which should be 2 since both threads have successfully incremented it.
# This fixes the issue of unexpected results due to multiple threads trying to access and modify the counter variable simultaneously.
In this updated code, we’re using Python’s `with lock:` statement to create a critical section around our increment function. This ensures that only one thread can access the `counter` variable at a time and prevents any potential race conditions or unexpected behavior.
Remember, they might seem boring at first glance but are crucial for any multi-threaded application. By using them properly, we can ensure that our code is thread-safe and doesn’t produce any unexpected results or crashes.