Nearby lessons
144 of 159Python - Semaphore and BoundedSemaphore
- Explain what a Semaphore controls
- Create a Semaphore object with a counter
- Understand how acquire() and release() change the counter
- Compare Semaphore with BoundedSemaphore
- Choose between Lock, RLock and Semaphore
What is Semaphore?
In the case of Lock and RLock, only one Thread is allowed to execute the protected code at a time.
Sometimes our requirement is different.
For example:
- At a time, 10 members may be allowed to access a Database Server.
- At a time, 4 members may be allowed to use a Network Connection.
For these types of requirements, Lock and RLock are not suitable.
In such situations, we should use:
Semaphore
Simple Definition:
A Semaphore is an advanced synchronization mechanism used to limit access to a shared resource having limited capacity.
Why Do We Need Semaphore?
With a normal Lock:
Shared Resource
│
▼
Only 1 Thread
Allowed
But suppose a Database Server can handle 10 connections simultaneously.
Database Server
│
├── Thread-1
├── Thread-2
├── Thread-3
├── ...
└── Thread-10
Allowing only one Thread would unnecessarily restrict the resource.
Semaphore allows us to specify exactly how many Threads can access the resource simultaneously.
Real-Life Examples
Example 1: Database Server
Suppose a Database Server allows a maximum of 10 users at the same time.
Semaphore(10)
Maximum Concurrent Access
=
10 Threads
Example 2: Network Connection
Suppose only four users are allowed to use a Network Connection simultaneously.
Semaphore(4)
Maximum Concurrent Access
=
4 Threads
These requirements can be handled using Semaphore.
Creating a Semaphore Object
A Semaphore object can be created as follows:
s = Semaphore(counter)
Here:
sis the Semaphore object reference.counterrepresents the maximum number of Threads allowed to access the resource simultaneously.
Example:
from threading import * s = Semaphore(3)
This means a maximum of 3 Threads can acquire the Semaphore at the same time.
Default Value of Semaphore Counter
The default value of the Semaphore counter is:
1
Therefore:
s = Semaphore()
is effectively created with an initial counter of 1.
Only one Thread can acquire it at a time.
Semaphore()
│
▼
Counter = 1
│
▼
Only One Thread
Allowed
Working of acquire() and release()
Semaphore maintains an internal counter.
Whenever a Thread executes:
s.acquire()
the available permit count decreases by 1.
Whenever a Thread executes:
s.release()
the available permit count increases by 1.
| Operation | Effect on Counter |
|---|---|
acquire() |
Counter decreases by 1 |
release() |
Counter increases by 1 |
Semaphore Counter Example
Suppose we create:
s = Semaphore(3)
Initially:
Counter = 3
Now three Threads acquire it:
Initial Counter = 3
│
▼
Thread-1 acquire()
Counter = 2
│
▼
Thread-2 acquire()
Counter = 1
│
▼
Thread-3 acquire()
Counter = 0
At this point, another Thread cannot acquire a permit immediately.
Thread-4 │ ▼ acquire() │ ▼ Counter = 0 │ ▼ Wait
When one of the active Threads calls:
s.release()
a permit becomes available and a waiting Thread can proceed.
Case 1 - Default Semaphore
Consider:
s = Semaphore()
No counter value is specified.
Therefore:
Counter = 1
Only one Thread can acquire the Semaphore at a time.
Thread-1 │ ▼ Acquire │ ▼ Counter = 0 │ ├──── Thread-2 → Wait └──── Thread-3 → Wait
This behavior is similar to the basic Lock concept for controlling concurrent access.
Case 2 - Semaphore with Counter
Consider:
s = Semaphore(3)
Here:
Counter = 3
Therefore, three Threads can acquire the Semaphore simultaneously.
Semaphore(3)
│
┌────┼────┐
▼ ▼ ▼
T1 T2 T3
Allowed
Any additional Thread must wait until one of these Threads releases a permit.
T4 ───► Wait T5 ───► Wait
Semaphore Example Program
Understanding Semaphore(2)
The most important statement in this program is:
s = Semaphore(2)
This means only 2 Threads can acquire the Semaphore simultaneously.
Five Threads are created:
| Thread | Argument |
|---|---|
t1 |
Dhoni |
t2 |
Yuvraj |
t3 |
Kohli |
t4 |
Rohit |
t5 |
Pandya |
But only two of these Threads can execute inside the Semaphore-protected section at the same time.
Program Output
Because Semaphore(2) allows two Threads simultaneously, output from two permitted Threads can be interleaved.
A possible output pattern can look like:
Good Evening:Good Evening:Dhoni Yuvraj Good Evening:Good Evening:Dhoni Yuvraj ...
After one permitted Thread completes its protected work and calls release(), another waiting Thread can enter.
The exact Thread order is not guaranteed because Thread scheduling can vary.
Complete Program Explanation
Step 1: Import Required Modules
from threading import * import time
The threading module provides Semaphore and Thread functionality.
The time module provides sleep().
Step 2: Create Semaphore
s = Semaphore(2)
The initial counter is 2.
Therefore, up to two Threads can acquire the Semaphore simultaneously.
Step 3: Define wish()
def wish(name):
Every Thread executes the same wish() function.
Step 4: Acquire Semaphore
s.acquire()
Before entering the protected work, each Thread attempts to acquire a Semaphore permit.
If a permit is available, the Thread proceeds.
If no permit is available, the Thread waits.
Step 5: Execute Loop
for i in range(10):
Each permitted Thread executes the loop ten times.
Step 6: Print Greeting
print("Good Evening:", end='')
The greeting is printed without moving to the next line.
Step 7: Sleep
time.sleep(2)
The Thread waits for two seconds.
While this Thread sleeps, another Thread that already holds another Semaphore permit can execute.
Step 8: Print Name
print(name)
The Thread prints its corresponding name.
Step 9: Release Semaphore
s.release()
After completing all ten iterations, the Thread releases its Semaphore permit.
The available counter increases, allowing another waiting Thread to continue.
Step 10: Create Five Threads
t1 = Thread(target=wish, args=("Dhoni",))
t2 = Thread(target=wish, args=("Yuvraj",))
t3 = Thread(target=wish, args=("Kohli",))
t4 = Thread(target=wish, args=("Rohit",))
t5 = Thread(target=wish, args=("Pandya",))
Five Threads are created, but the Semaphore permits only two concurrent acquisitions.
Step 11: Start All Threads
t1.start() t2.start() t3.start() t4.start() t5.start()
All five Threads request access.
Two Threads get permits.
The remaining three Threads wait.
Execution Flow of Semaphore(2)
Program Starts
│
▼
Create Semaphore(2)
│
▼
Counter = 2
│
▼
Create 5 Threads
│
▼
Start All Threads
│
▼
Thread-1 acquire()
Counter = 1
│
▼
Thread-2 acquire()
Counter = 0
│
├──────── Thread-3 waits
├──────── Thread-4 waits
└──────── Thread-5 waits
│
▼
Two Threads Execute
wish()
│
▼
One Thread Completes
│
▼
release()
│
▼
Permit Becomes Available
│
▼
One Waiting Thread
Acquires Permit
│
▼
Process Continues
│
▼
All Threads Complete
Lock vs RLock vs Semaphore
| Feature | Lock | RLock | Semaphore |
|---|---|---|---|
| Concurrent acquisitions | 1 | 1 Thread at a time | Fixed number based on counter |
| Same owner can acquire repeatedly | No | Yes | Controlled by available permits |
| Main purpose | Mutual exclusion | Reentrant mutual exclusion | Limited-capacity access |
| Recursive functions | Not suitable for repeated acquisition | Suitable | Not its primary purpose |
| Example | One shared resource | Recursive protected operation | Limited database connections |
BoundedSemaphore
A normal Semaphore allows its internal count to be increased beyond its initial value by extra release() calls.
This can hide programming mistakes.
Python therefore provides:
BoundedSemaphore
BoundedSemaphore works almost like a normal Semaphore.
The important difference is that it checks that releases do not increase the Semaphore beyond its initial bound.
If too many release() calls are made, Python raises:
ValueError: Semaphore released too many times
Normal Semaphore Example
Normal Semaphore Output
Why Does Normal Semaphore Allow This?
In this program:
Initial Counter = 2 acquire() → 2 times release() → 4 times
A normal Semaphore permits extra release() calls, so the internal counter can rise above the original value.
Therefore, the program completes and prints:
End
This is why the document describes normal Semaphore as an unlimited Semaphore in this context.
Normal Semaphore Counter Flow
Semaphore(2)
Counter = 2
│
▼
acquire()
Counter = 1
│
▼
acquire()
Counter = 0
│
▼
release()
Counter = 1
│
▼
release()
Counter = 2
│
▼
release()
Counter = 3
│
▼
release()
Counter = 4
│
▼
Print "End"
A normal Semaphore does not enforce the original upper bound when release() is called.
BoundedSemaphore Example
BoundedSemaphore Output
Why is the BoundedSemaphore Program Invalid?
The program starts with:
s = BoundedSemaphore(2)
Therefore, its maximum bound is 2.
The program performs:
acquire() → 2 times release() → 4 times
After two valid releases, the Semaphore has returned to its original bound.
The next extra release attempts to exceed that bound.
Therefore, Python raises:
ValueError: Semaphore released too many times
BoundedSemaphore Execution Flow
BoundedSemaphore(2)
│
▼
Initial Value = 2
│
▼
acquire()
Value = 1
│
▼
acquire()
Value = 0
│
▼
release()
Value = 1
│
▼
release()
Value = 2
│
▼
release() Again
│
▼
Would Exceed
Initial Bound
│
▼
ValueError:
Semaphore released
too many times
Semaphore vs BoundedSemaphore
| Semaphore | BoundedSemaphore |
|---|---|
| Controls access using a counter. | Also controls access using a counter. |
Extra release() calls can increase the counter beyond its initial value. |
Counter cannot be increased beyond its initial bound. |
| Extra releases may hide programming mistakes. | Extra releases are detected. |
Extra release() can be accepted. |
Extra release() raises ValueError. |
Recommendation from the Document
The document recommends using:
BoundedSemaphore
instead of a normal Semaphore when we want to prevent simple programming mistakes involving extra release() calls.
Semaphore
│
▼
Extra release()
May Go Undetected
BoundedSemaphore
│
▼
Extra release()
│
▼
ValueError
Difference Between Lock and Semaphore
| Lock | Semaphore |
|---|---|
| Only one Thread can acquire the Lock at a time. | A fixed number of Threads, specified by the counter value, can acquire the Semaphore at the same time. |
For example:
Lock() │ ▼ 1 Thread Semaphore(3) │ ▼ Up to 3 Threads
Advantage of Synchronization
The main advantage of synchronization is:
Synchronization helps overcome data inconsistency problems.
Multiple Threads
│
▼
Synchronization
│
▼
Controlled Access
│
▼
Reduce Data
Inconsistency Problems
Disadvantage of Synchronization
Synchronization also has disadvantages.
According to the document, synchronization:
- Increases the waiting time of Threads.
- Creates performance problems.
Synchronization
│
├──► Threads May Wait
│
└──► Performance Overhead
Therefore, if there is no specific requirement for synchronization, it should not be added unnecessarily.
Choosing Lock, RLock or Semaphore
| Requirement | Suitable Mechanism |
|---|---|
| Only one Thread should access the resource | Lock |
| Owner Thread must acquire the same Lock repeatedly | RLock |
| Fixed number of Threads should access simultaneously | Semaphore |
| Semaphore should detect excessive releases | BoundedSemaphore |
Complete Execution Concept
Synchronization
│
├───────────────┐
│ │
▼ ▼
Lock RLock
│ │
▼ ▼
One Thread One Thread
at a Time at a Time
│
▼
Owner Can
Re-acquire
Semaphore
│
▼
Fixed Number of
Threads at a Time
│
▼
Semaphore(counter)
BoundedSemaphore
│
▼
Semaphore with
Release Bound Check
Summary
- A Semaphore is an advanced synchronization mechanism.
- It is useful when a shared resource has limited capacity.
- A Semaphore is created using:
s = Semaphore(counter)
- The counter specifies how many Threads may acquire the Semaphore simultaneously.
- The default counter value is
1. - Every
acquire()decreases the available count by 1. - Every
release()increases the available count by 1. Semaphore()allows only one Thread at a time.Semaphore(3)permits up to three Threads to acquire it simultaneously.- The example uses
Semaphore(2)with five Threads. - Two Threads can execute the protected
wish()work simultaneously while the other three wait. - A normal Semaphore permits its count to increase beyond its initial value through extra releases.
BoundedSemaphoreprevents the count from exceeding its initial bound.- Excessive release on
BoundedSemaphoreraises:
ValueError: Semaphore released too many times
- Synchronization helps overcome data inconsistency problems.
- Synchronization also increases waiting time and may affect performance.
Important Notes
- Lock and RLock permit only one Thread at a time to hold the synchronization object.
- Semaphore is useful when multiple Threads should be allowed simultaneously.
- The Semaphore counter specifies the maximum number of simultaneous acquisitions.
- The default Semaphore counter is
1. acquire()decreases the available counter by 1.release()increases the available counter by 1.- When no permit is available, additional Threads wait.
Semaphore(2)permits two Threads simultaneously.- In the example, five Threads are created: Dhoni, Yuvraj, Kohli, Rohit, and Pandya.
- Only two of these Threads can acquire the Semaphore simultaneously.
- The remaining Threads wait until a permit becomes available.
- A normal Semaphore can accept extra
release()calls and its counter can exceed its initial value. BoundedSemaphoredetects releases that would exceed its original bound.- Excessive releases on BoundedSemaphore raise
ValueError: Semaphore released too many times. - The document recommends BoundedSemaphore to help prevent simple programming mistakes.
- Lock permits one Thread, whereas Semaphore permits a fixed number of Threads according to its counter.
- The main advantage of synchronization is avoiding data inconsistency problems.
- The disadvantages are increased waiting time and possible performance problems.
- Synchronization should be used when there is a specific requirement for controlled shared-resource access.
- Semaphore(n) allows n threads into the critical section at once
- acquire() decrements the counter; release() increments it
- BoundedSemaphore raises ValueError if released more times than acquired
- Lock allows one thread, Semaphore allows a fixed number