Nearby lessons

144 of 159

Python - Semaphore and BoundedSemaphore

📌 What You Will Learn
  • 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:

  • s is the Semaphore object reference.
  • counter represents 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

🐍Code Cell
1from threading import *
2import time
3 
4s = Semaphore(2)
5 
6def wish(name):
7 s.acquire()
8 
9 for i in range(10):
10 print("Good Evening:", end='')
11 time.sleep(2)
12 print(name)
13 
14 s.release()
15 
16t1 = Thread(target=wish, args=("Dhoni",))
17t2 = Thread(target=wish, args=("Yuvraj",))
18t3 = Thread(target=wish, args=("Kohli",))
19t4 = Thread(target=wish, args=("Rohit",))
20t5 = Thread(target=wish, args=("Pandya",))
21 
22t1.start()
23t2.start()
24t3.start()
25t4.start()
26t5.start()
Output
No output captured.

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

🐍Code Cell
1from threading import *
2 
3s = Semaphore(2)
4 
5s.acquire()
6s.acquire()
7 
8s.release()
9s.release()
10s.release()
11s.release()
12 
13print("End")
Output
No output captured.

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

🐍Code Cell
1from threading import *
2 
3s = BoundedSemaphore(2)
4 
5s.acquire()
6s.acquire()
7 
8s.release()
9s.release()
10s.release()
11s.release()
12 
13print("End")
Output
No output captured.

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.
  • BoundedSemaphore prevents the count from exceeding its initial bound.
  • Excessive release on BoundedSemaphore raises:
ValueError: Semaphore released too many times
  • Synchronization helps overcome data inconsistency problems.
  • Synchronization also increases waiting time and may affect performance.

Important Notes

  1. Lock and RLock permit only one Thread at a time to hold the synchronization object.
  2. Semaphore is useful when multiple Threads should be allowed simultaneously.
  3. The Semaphore counter specifies the maximum number of simultaneous acquisitions.
  4. The default Semaphore counter is 1.
  5. acquire() decreases the available counter by 1.
  6. release() increases the available counter by 1.
  7. When no permit is available, additional Threads wait.
  8. Semaphore(2) permits two Threads simultaneously.
  9. In the example, five Threads are created: Dhoni, Yuvraj, Kohli, Rohit, and Pandya.
  10. Only two of these Threads can acquire the Semaphore simultaneously.
  11. The remaining Threads wait until a permit becomes available.
  12. A normal Semaphore can accept extra release() calls and its counter can exceed its initial value.
  13. BoundedSemaphore detects releases that would exceed its original bound.
  14. Excessive releases on BoundedSemaphore raise ValueError: Semaphore released too many times.
  15. The document recommends BoundedSemaphore to help prevent simple programming mistakes.
  16. Lock permits one Thread, whereas Semaphore permits a fixed number of Threads according to its counter.
  17. The main advantage of synchronization is avoiding data inconsistency problems.
  18. The disadvantages are increased waiting time and possible performance problems.
  19. Synchronization should be used when there is a specific requirement for controlled shared-resource access.
📝 Key Takeaways
  • 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

🧠 Test Your Knowledge

15 Questions
Progress: 0 / 15