This article will introduce the most basic 5 different multi-threading methods implementation in Python. We will use a very simple example to show how to make use of these 5 different methods to solve a problem.
Barrier
Barrier objects in python are used to wait for a fixed number of thread to complete execution before any particular thread can proceed forward with the execution of the program. Each thread calls wait() function upon reaching the barrier. The barrier is responsible for keeping track of the number of wait() calls. If this number goes beyond the number of threads for which the barrier was initialized with, then the barrier gives a way to the waiting threads to proceed on with the execution. All the threads at this point of execution, are simultaneously released.
There are several main methods available from this object:
parties()
A number of threads required to reach the common barrier point.
n_waiting()
Number of threads waiting in the common barrier point
broken()
A boolean value, True- if the barrier is in the broken state else False.
####wait( timeout = None)
Wait until notified or a timeout occurs. If the calling thread has not acquired the lock when this method is called, a runtime error is raised.
This method releases the underlying lock and then blocks until it is awakened by a notify() or notify_all() method call for the same condition variable in another thread, or until the optional timeout occurs. Once awakened or timed out, it re-acquires the lock and returns.
When the timeout argument is present and not None, it should be a floating point number specifying a timeout for the operation in seconds (or fractions thereof).
reset()
Set or return the barrier to the default state .i.e. empty state. And threads waiting on it will receive the BrokenBarrierError.
bort()
This will put the barrier into a broken state. This causes all the active threads or any future calls to wait() to fail with the BrokenBarrierError.
Here is a program to show how barrier works in python
1 | import threading |
Lock
The threading module of Python includes locks as a synchronization tool. A lock has two states:
locked and unlocked.
A lock can be locked using the acquire() method. Once a thread has acquired the lock, all subsequent attempts to acquire the lock are blocked until it is released. The lock can be released using the release() method.
Calling the release() method on a lock, in an unlocked state, results in an error.
Let us take an example to see how to make use lock to solve a race condition problem.
Race condition is a significant problem in concurrent programming. The condition occurs when one thread tries to modify a shared resource at the same time that another thread is modifying that resource – this leads to garbled output, which is why threads need to be synchronized.
1 | # Importing the threading module |
Let us use a list variable to record the whole process how deposit changes alone the way.
If we do not have the lock.acquire() and lock.release(), the result may looks like below when there is no lock.acquire() and lock.release() present:
With the usage of lock.acquire() and lock.release(), it will be like:
Semaphore Object
A semaphore manages an internal counter which is decremented by each acquire() call and incremented by each release() call. The counter can never go below zero; when acquire() finds that it is zero, it blocks, waiting until some other thread calls release().
There are several main methods available from this object:
acquire(blocking=True, timeout=None):
release()
Mutex(Lock) VS Semaphore
As we can tell, the methods we have from Lock instance are pretty similar to the methods we have from Semaphore. Then what is the difference between Mutex and Semaphore ?
Mutex is a mutual exclusion object that synchronizes access to a resource. It is created with a unique name at the start of a program. The Mutex is a locking mechanism that makes sure only one thread can acquire the Mutex at a time and enter the critical section. This thread only releases the Mutex when it exits the critical section.
A semaphore is a signalling mechanism and a thread that is waiting on a semaphore can be signaled by another thread. This is different than a mutex as the mutex can be signaled only by the thread that called the wait function.
There are mainly two types of semaphores i.e. counting semaphores and binary semaphores.
Counting Semaphores are integer value semaphores and have an unrestricted value domain. These semaphores are used to coordinate the resource access, where the semaphore count is the number of available resources.
The binary semaphores are like counting semaphores but their value is restricted to 0 and 1. The wait operation only works when the semaphore is 1 and the signal operation succeeds when semaphore is 0.
Event Object
One thread can signal an event and other threads wait for it.
There are several main methods available from this object:
isSet()
Return true if and only if the internal flag is true
set()
Set the internal flag to true. All threads waiting for it to become true are awakened. Threads that call wait() once the flag is true will not block at all.
clear()
Reset the internal flag to false. Subsequently, threads calling wait() will block until set() is called to set the internal flag to true again.
wait([timeout in seconds])
Block until the internal flag is true.
Condition
There are several main methods available from this object:
Condition([lock])
If the lock argument is given and not None, it must be a Lock or RLock object, and it is used as the underlying lock. Otherwise, a new RLock object is created and used as the underlying lock.
acquire(*args)
Acquire the underlying lock. This method calls the corresponding method on the underlying lock; the return value is whatever that method returns.
release ()
Release the underlying lock. This method calls the corresponding method on the underlying lock; there is no return value.
wait([timeout])
notify()
Wake up a thread waiting on this condition, if any. This must only be called when the calling thread has acquired the lock.
notifyAll()
Wake up all threads waiting on this condition.
Example
Ok. At the end, let see a problem which can be solved by making use Brutal force, Barrier, Event, Condition, Lock and Semaphore.
Problem description
Suppose we have a class:
1 | class Foo(object): |
Now we have an instance of Foo which will be passed to 3 different threads. Thread A will call first(), thread B will call second(), thread C will call third(). Design a mechanism to ensure that the second() is executed after first(), third() is executed after second().
Brutal Force
We definite can make use of a very brutal force way to solve this problem by setting a flag with in the class. Until the flag was trigger, we are not able to move forward. Refer to the code below:
1 | import time |
Barrier
Raise two barriers. Both wait for two threads to reach them.
First thread can print before reaching the first barrier. Second thread can print before reaching the second barrier. Third thread can print after the second barrier.
1 | from threading import Barrier |
Lock (Mutex)
Start with two locked locks. First thread unlocks the first lock that the second thread is waiting on. Second thread unlocks the second lock that the third thread is waiting on.
1 | from threading import Lock |
Semaphore
Start with two closed gates represented by 0-value semaphores. Second and third thread are waiting behind these gates. When the first thread prints, it opens the gate for the second thread. When the second thread prints, it opens the gate for the third thread.
1 | from threading import Semaphore |
Event
Set events from first and second threads when they are done. Have the second thread wait for first one to set its event. Have the third thread wait on the second thread to raise its event.
1 | from threading import Event |
Condition
Have all three threads attempt to acquire an RLock via Condition. The first thread can always acquire a lock, while the other two have to wait for the order to be set to the right value. First thread sets the order after printing which signals for the second thread to run. Second thread does the same for the third.
1 | from threading import Condition |
Referrence:
https://leetcode.com/problems/print-in-order/discuss/335939/5-Python-threading-solutions-(Barrier-Lock-Event-Semaphore-Condition)-with-explanation
https://www.geeksforgeeks.org/barrier-objects-python/
https://www.educative.io/edpresso/what-are-locks-in-python