Last Updated on November 14, 2023
A semaphore is a concurrency primitive that is used to signal between concurrent tasks.
Asyncio provides semaphores via the asyncio.Semaphore class.
Semaphores are configurable and versatile, allowing them to be used like a mutex to protect a critical section but also to be used as a coroutine-safe counter or a gate to protect a limited resource.
In this tutorial, you will discover how to use semaphores in asyncio.
After completing this tutorial, you will know:
- What is a semaphore and how can they be used in concurrent programming.
- How to use a semaphore among coroutines and tasks in asyncio.
- How to develop examples to explore the capabilities of semaphores with concurrent tasks.
Let’s get started.
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 protected by a mutex lock that is decremented each time the semaphore is acquired and incremented each time it is released.
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.
Although the semaphore is described in terms of threads, it is a primitive that can be used with other units of concurrency.
For example, the threading.Semaphore class allows us to use the semaphore with threads:
The multiprocessing.Semaphore class allows the use of a semaphore with processes:
And the asyncio.Semaphore class allows us to use a semaphore with coroutines in asyncio programs.
Now that we know what a semaphore is, let’s look at how we might use it in asyncio.
Run loops using all CPUs, download your FREE book to learn how.
How to Use an Asyncio Semaphore
Python provides a semaphore via the asyncio.Semaphore class for use with coroutines.
The asyncio.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 coroutines that can hold the semaphore at a time.
For example, we might want to set it to 100:
1 2 3 |
... # create a semaphore with a limit of 100 semaphore = asyncio.Semaphore(100) |
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 task calls release().
— Asyncio Synchronization Primitives
The semaphore can be acquired by calling the acquire() function.
This returns a coroutine that must be awaited.
Acquire a semaphore. If the internal counter is greater than zero, decrement it by one and return True immediately. If it is zero, wait until a release() is called and return True.
— Asyncio Synchronization Primitives
For example:
1 2 3 |
... # acquire the semaphore await semaphore.acquire() |
Once acquired, the semaphore can be released again by calling the release() function.
Release a semaphore, incrementing the internal counter by one. Can wake up a task waiting to acquire the semaphore.
— Asyncio Synchronization Primitives
For example:
1 2 3 |
... # release the semaphore semaphore.release() |
The release() method can be called many times, e.g. more times than the acquire() method was called.
Unlike BoundedSemaphore, Semaphore allows making more release() calls than acquire() calls.
— Asyncio Synchronization Primitives
Each time release() is called, it will increment the internal counter of the semaphore, regardless of the initial value. This can be used to increase the capacity of the semaphore dynamically.
For example:
1 2 3 4 5 6 7 8 9 10 |
... # acquire the semaphore semaphore.acquire() # do something # ... # release the semaphore semaphore.release() # increase the capacity of the semaphore for i in range(5): semaphore.release() |
The asyncio.Semaphore class supports the context manager interface, 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 async with semaphore: # ... |
Finally, a coroutine can check if the semaphore is current at capacity via the locked() method.
It will return True if the semaphore cannot be acquired at the time of the call, or False otherwise.
For example:
1 2 3 4 |
... # check if there is space on the semaphore if not semaphore.locked(): # ... |
Now that we know how to use the asyncio.Semaphore in Python, let’s look at a worked example.
Example of Using an Asyncio Semaphore
We can explore how to use an asyncio.Semaphore with a worked example.
We will develop an example with a suite of coroutines but a limit on the number of coroutines that can perform an action simultaneously.
A semaphore will be used to limit the number of concurrent coroutines which will be less than the total number of coroutines, allowing some coroutines to block, wait for a position, then be notified and acquire a position.
The complete example 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 25 26 27 |
# SuperFastPython.com # example of using an asyncio semaphore from random import random import asyncio # task coroutine async def task(semaphore, number): # acquire the semaphore async with semaphore: # generate a random value between 0 and 1 value = random() # block for a moment await asyncio.sleep(value) # report a message print(f'Task {number} got {value}') # main coroutine async def main(): # create the shared semaphore semaphore = asyncio.Semaphore(2) # create and schedule tasks tasks = [asyncio.create_task(task(semaphore, i)) for i in range(10)] # wait for all tasks to complete _ = await asyncio.wait(tasks) # start the asyncio program asyncio.run(main()) |
Running the example first creates the main() coroutine that is used as the entry point into the asyncio program.
The main() coroutine runs and first creates the shared semaphore with an initial counter value of 2, meaning that two coroutines can hold the semaphore at once.
The main() coroutine then creates and schedules 10 tasks to execute our task() coroutine, passing the shared semaphore and a unique number between 0 and 9.
The main() coroutine then suspends and waits for all tasks to complete.
The tasks run one at a time.
Each task first attempts to acquire the semaphore. If there is a position available it proceeds with the block, otherwise it waits for a position to become available.
Once acquired, a task generates a random value, blocks for a moment, and then reports the generated value. It then releases the semaphore and terminates. The semaphore is not released while the task is suspended in the call to sleep().
The body of the semaphore context manager is limited to two semaphores at a time.
This highlights how we can limit the number of coroutines to execute a block of code concurrently.
The output from the program will differ each time it is run given the use of random numbers.
1 2 3 4 5 6 7 8 9 10 |
Task 0 got 0.20369168197618748 Task 2 got 0.20640107131350838 Task 1 got 0.6855263719449817 Task 3 got 0.9396433586858612 Task 4 got 0.8039832235015294 Task 6 got 0.12266060196253203 Task 5 got 0.879466225105295 Task 7 got 0.6675244153844875 Task 8 got 0.11511060306129695 Task 9 got 0.9607702805925814 |
Free Python Asyncio Course
Download your FREE Asyncio PDF cheat sheet and get BONUS access to my free 7-day crash course on the Asyncio API.
Discover how to use the Python asyncio module including how to define, create, and run new coroutines and how to use non-blocking I/O.
Further Reading
This section provides additional resources that you may find helpful.
Python Asyncio Books
- Python Asyncio Mastery, Jason Brownlee (my book!)
- Python Asyncio Jump-Start, Jason Brownlee.
- Python Asyncio Interview Questions, Jason Brownlee.
- Asyncio Module API Cheat Sheet
I also recommend the following books:
- Python Concurrency with asyncio, Matthew Fowler, 2022.
- Using Asyncio in Python, Caleb Hattingh, 2020.
- asyncio Recipes, Mohamed Mustapha Tahrioui, 2019.
Guides
APIs
- asyncio — Asynchronous I/O
- Asyncio Coroutines and Tasks
- Asyncio Streams
- Asyncio Subprocesses
- Asyncio Queues
- Asyncio Synchronization Primitives
References
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Takeaways
You now know how to use a semaphore with coroutines in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Andres Grijalva on Unsplash
Do you have any questions?