Last Updated : 15 Sep, 2023
This article discusses the concept of thread synchronization in case of
multithreadingin Python programming language.
Synchronization between threads
Thread synchronization is defined as a mechanism which ensures that two or more concurrent threads do not simultaneously execute some particular program segment known as
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 try to access shared resource or critical section at the same time.
Concurrent accesses to shared resource can lead to
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 race condition:
Python
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))
Output:
Iteration 0: x = 175005 Iteration 1: x = 200000 Iteration 2: x = 200000 Iteration 3: x = 169432 Iteration 4: x = 153316 Iteration 5: x = 200000 Iteration 6: x = 167322 Iteration 7: x = 200000 Iteration 8: x = 169917 Iteration 9: x = 153589
In above program:
The expected final value of
xis 200000 but what we get in 10 iterations of
main_taskfunction is some different values. This happens due to concurrent access of threads to the shared variable
x. This unpredictability in value of
xis nothing but
race condition. Given below is a diagram which shows how can
race conditionoccur in above program:
Notice that expected value of
xin above diagram is 12 but due to race condition, it turns out to be 11!
Hence, we need a tool for proper synchronization between multiple threads.
Using Locks
threadingmodule provides a
Lockclass to deal with the race conditions. Lock is implemented using a
Semaphoreobject 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.Lock
class provides following methods:
Consider the example given below:
Python
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 thread
calls increment function 100000 times.
"""
for _ in range(100000):
lock.acquire()
increment()
lock.release()
def main_task():
global x
# setting global variable x as 0
x = 0
# creating a lock
lock = threading.Lock()
# creating threads
t1 = threading.Thread(target=thread_task, args=(lock,))
t2 = threading.Thread(target=thread_task, args=(lock,))
# 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))
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 us try to understand the above code step by step:
lock = threading.Lock()
t1 = threading.Thread(target=thread_task, args=(lock,)) t2 = threading.Thread(target=thread_task, args=(lock,))
lock.acquire() increment() lock.release()As you can see in the results, the final value of x comes out to be 200000 every time (which is the expected final result).
Here is a diagram given below which depicts the implementation of locks in above program:
This brings us to the end of this tutorial series on
Multithreading in Python. Finally, here are a few advantages and disadvantages of multithreading:
Advantages:
Disadvantages:
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4