Last Updated on July 29, 2023
You can use a semaphore in Python by threading.Semaphore class.
In this tutorial you will discover how to use a semaphore for concurrent programming.
Let’s get started.
Need for a Semaphore
A thread is a thread of execution in a computer program.
Every Python program has at least one thread of execution called the main thread. Both processes and threads are created and managed by the underlying operating system.
Sometimes we may need to create additional threads in our program in order to execute code concurrently.
Python provides the ability to create and manage new threads via the threading module and the threading.Thread class.
You can learn more about Python threads in the guude:
In concurrent programming, we may need to limit the number of concurrent threads that can perform an action simultaneously, such as execute a critical section, perform a calculation, or operate an external resource.
This could be achieved with a mutual exclusion lock and a counter that would have to be manually maintained.
An alternative is to use a semaphore.
What is a semaphore and how can we use it in Python?
Run loops using all CPUs, download your FREE book to learn how.
What is a Semaphore
A semaphore is a concurrency primitive that allows a limit on the number of threads that can acquire a lock protecting a critical section.
It is an extension of a mutual exclusion (mutex) lock that adds a count for the number of threads that can acquire the lock before additional threads will block. Once full, new threads can only acquire a position on the semaphore once an existing thread holding the semaphore releases a position.
Internally, the semaphore maintains a counter initialized to a provided value, protected by a mutex lock that is decremented each time the semaphore is acquired and incremented each time it is released.
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().
— threading — Thread-based parallelism
When a semaphore is created, the upper limit on the counter is set. If it is set to 1, then the semaphore will operate like a mutex lock.
A semaphore provides a useful concurrency tool for limiting the number of threads that can access a resource concurrently. Some examples include:
- Limiting concurrent socket connections to a server.
- Limiting concurrent file operations on a hard drive.
- Limiting concurrent calculations.
Now that we know what a semaphore is, let’s look at how we might use it in Python.
How to Use a Semaphore
Python provides a semaphore via the threading.Semaphore class.
The threading.Semaphore instance must be configured when it is created to set the limit on the internal counter. This limit will match the number of concurrent threads that can hold the semaphore.
For example, we might want to set it to 100:
1 2 3 |
... # create a semaphore with a limit of 100 semaphore = Semaphore(100) |
In this implementation, each time the semaphore is acquired, the internal counter is decremented. Each time the semaphore is released, the internal counter is incremented. The semaphore cannot be acquired if the semaphore has no available positions in which case, threads attempting to acquire it must block until a position becomes available.
The semaphore can be acquired by calling the acquire() function, for example:
1 2 3 |
... # acquire the semaphore semaphore.acquire() |
By default, it is a blocking call, which means that the calling thread will block until a position becomes available on the semaphore.
The “blocking” argument can be set to False in which case, if a position is not available on the semaphore, the thread will not block and the function will return immediately, returning a False value indicating that the semaphore was not acquired or a True value if a position could be acquired.
1 2 3 |
... # acquire the semaphore without blocking semaphore.acquire(blocking=False) |
The “timeout” argument can be set to a number of seconds that the calling thread is willing to wait for a position on the semaphore if one is not available, before giving up. Again, the acquire() function will return a value of True if a position could be acquired or False otherwise.
1 2 3 |
... # acquire the semaphore with a timeout semaphore.acquire(timeout=10) |
Once acquired, the semaphore can be released again by calling the release() function.
1 2 3 |
... # release the semaphore semaphore.release() |
More than one position can be made available by calling release and setting the “n” argument to an integer number of positions to release on the semaphore.
This might be helpful if it is known a thread has died without correctly releasing the semaphore, or if one thread acquires the same semaphore more than once.
Do not use this argument unless you have a clear use case, it is likely to get you into trouble with a semaphore left in an inconsistent state.
1 2 3 |
... # release three positions on the semaphore semaphore.release(n=3) |
Finally, the threading.Semaphore class supports usage via the context manager, which will automatically acquire and release the semaphore for you. As such it is the preferred usage, if appropriate for your program.
For example:
1 2 3 4 |
... # acquire the semaphore with semaphore: # ... |
It is possible for the release() method to be called more times than the acquire() method, and this will not cause a problem. It is a way of increasing the capacity of the semaphore.
A threading.BoundedSemaphore class can be used which will check to ensure that the internal counter does not go above the initial value via calls to release(). If it does, a ValueError will be raised, treating it as an error condition.
Now that we know how to use the threading.Semaphore in Python, let’s look at a worked example.
Free Python Threading Course
Download your FREE threading PDF cheat sheet and get BONUS access to my free 7-day crash course on the threading API.
Discover how to use the Python threading module including how to create and start new threads and how to use a mutex locks and semaphores
Example of Using a Semaphore
We can explore how to use a threading.Semaphore with a worked example.
We will develop an example with a suite of threads but a limit on the number of threads that can perform an action simultaneously. A semaphore will be used to limit the number of concurrent threads which will be less than the total number of threads, allowing some threads to block, wait for a position, then be notified and acquire a position.
First, we can define a target task function that takes the shared semaphore and a unique integer as arguments. The function will attempt to acquire the semaphore, and once a position is acquired it will simulate some processing by generating a random number and blocking for a moment, then report its data and release the semaphore.
The complete task() function is listed below.
1 2 3 4 5 6 7 8 9 |
# target function def task(semaphore, number): # attempt to acquire the semaphore with semaphore: # process value = random() sleep(value) # report result print(f'Thread {number} got {value}') |
The main thread will first create the threading.Semaphore instance and limit the number of concurrent processing threads to 2.
1 2 3 |
... # create a semaphore semaphore = Semaphore(2) |
Next, we will create and start 10 threads and pass each the shared semaphore instance and a unique integer to identify the thread.
1 2 3 4 5 |
... # create a suite of threads for i in range(10): worker = Thread(target=task, args=(semaphore, i)) worker.start() |
The main thread will then wait for all worker threads to complete before exiting.
Tying this together, the complete example of using a semaphore is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# SuperFastPython.com # example of using a semaphore from time import sleep from random import random from threading import Thread from threading import Semaphore # target function def task(semaphore, number): # attempt to acquire the semaphore with semaphore: # process value = random() sleep(value) # report result print(f'Thread {number} got {value}') # create a semaphore semaphore = Semaphore(2) # create a suite of threads for i in range(10): worker = Thread(target=task, args=(semaphore, i)) worker.start() # wait for all workers to complete... |
Running the example first creates the semaphore instance then starts ten worker threads.
All ten threads attempt to acquire the semaphore, but only two threads are granted positions at a time. The threads on the semaphore do their work and release the semaphore when they are done, at random intervals.
Each release of the semaphore (via the context manager) allows another thread to acquire a position and perform its calculation, all the while allowing only two of the threads to be processed at any one time, even though all ten threads are executing their run methods.
Note, your specific values will differ given the use of random numbers.
1 2 3 4 5 6 7 8 9 10 |
Thread 1 got 0.4468840323081351 Thread 0 got 0.7288038062917327 Thread 2 got 0.4497887327563145 Thread 4 got 0.019601471581036645 Thread 3 got 0.5114751539092154 Thread 6 got 0.6191428550062478 Thread 5 got 0.9893921843198458 Thread 8 got 0.022640379341017924 Thread 7 got 0.20649643092073067 Thread 9 got 0.18636311846540998 |
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
More Help With Semaphores
If you need more help with semaphores, some of these tutorials may be interesting:
Semaphores with threads:
Semaphores with processes:
Semaphores with asyncio:
Further Reading
This section provides additional resources that you may find helpful.
Python Threading Books
- Python Threading Jump-Start, Jason Brownlee (my book!)
- Threading API Interview Questions
- Threading Module API Cheat Sheet
I also recommend specific chapters in the following books:
- Python Cookbook, David Beazley and Brian Jones, 2013.
- See: Chapter 12: Concurrency
- Effective Python, Brett Slatkin, 2019.
- See: Chapter 7: Concurrency and Parallelism
- Python in a Nutshell, Alex Martelli, et al., 2017.
- See: Chapter: 14: Threads and Processes
Guides
- Python Threading: The Complete Guide
- Python ThreadPoolExecutor: The Complete Guide
- Python ThreadPool: The Complete Guide
APIs
References
Takeaways
You now know how to use a semaphore in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Andy zhou says
In part “What is a Semaphore” pagraph 3:
“Internally, the semaphore maintains a counter protected by a mutex lock that is incremented each time the semaphore is acquired and decremented each time it is released.”
I think you got it backwards.
Jason Brownlee says
Thank you! Fixed.