Last Updated on November 14, 2023
It is common to need to share a variable between concurrent tasks that may be set and checked.
Asyncio provides a concurrency primitive that provides exactly this called an event via the asyncio.Event class. It is essentially a mutex lock and a boolean variable, but also offers the ability for a calling coroutine to wait for the event to be set, a capability not provided by a simple mutex.
In this tutorial, you will discover how to use an event concurrency primitive in asyncio.
After completing this tutorial, you will know:
- What is an event concurrency primitive and why we cannot just use a boolean variable.
- How to use asyncio event shared between coroutines and tasks.
- How an asyncio event might be used to trigger behavior across multiple concurrent tasks.
Let’s get started.
What is an Asyncio Event
An event provides a way to notify coroutines that something has happened.
This is achieved using a coroutine-safe manner that avoids race conditions.
An asyncio event can be used to notify multiple asyncio tasks that some event has happened.
— Asyncio Synchronization Primitives
An event manages an internal boolean flag that can be either set or not set.
Coroutines can check the status of the event, change the status of the event or wait on the event for it to be set.
Python provides events for other units of concurrency, such as threads via the threading.Event class:
It also provides an event for notifying processes via the multiprocessing.Event class:
The asyncio.Event provides similar functionality for use with coroutines, instead of threads or processes. Importantly, the asyncio.Event is not thread-safe, i.e. only coroutine-safe.
An event object. Not thread-safe.
— Asyncio Synchronization Primitives
Now that we know what an event is, let’s look at how we might use it in an asyncio program.
Run loops using all CPUs, download your FREE book to learn how.
How to Use an Asyncio Event
An event is a simple concurrency primitive that allows communication between coroutines.
An asyncio.Event object wraps a boolean variable that can either be “set” (True) or “not set” (False).
Coroutines sharing the event instance can check if the event is set, set the event, clear the event (make it not set), or wait for the event to be set.
The asyncio.Event provides an easy way to share a boolean variable between coroutines that can act as a trigger for an action.
An Event object manages an internal flag that can be set to true with the set() method and reset to false with the clear() method. The wait() method blocks until the flag is set to true. The flag is set to false initially.
— Asyncio Synchronization Primitives
First, an event object must be created and the event will be in the “not set” state.
1 2 3 |
... # create an instance of an event event = asyncio.Event() |
Once created we can check if the event has been set via the is_set() function which will return True if the event is set, or False otherwise.
For example:
1 2 3 4 |
... # check if the event is set if event.is_set(): # do something... |
The event can be set via the set() function. Any coroutines waiting on the event to be set will be notified.
For example:
1 2 3 |
... # set the event event.set() |
The event can be marked as “not set” (whether it is currently set or not) via the clear() function.
1 2 3 |
... # mark the event as not set event.clear() |
Finally, coroutines can wait for the event to be set via the wait() function.
Calling this function will block until the event is marked as set (e.g. another coroutine calling the set() function). If the event is already set, the wait() function will return immediately.
1 2 3 |
... # wait for the event to be set await event.wait() |
Now that we know how to use an asyncio.Event, let’s look at a worked example.
Example of an Asyncio Event
We can explore how to use an asyncio.Event object.
In this example, we will create a suite of coroutines that each will perform some processing and report a message. All coroutines will use an event to wait to be set before starting their work. The main coroutine will set the event and trigger the processing in all coroutines.
First, we can define a task coroutine that takes the shared asyncio.Event instance and a unique integer to identify the task.
1 2 3 |
# task coroutine async def task(event, number): # ... |
Next, the function will wait for the event to be set before starting the processing work.
1 2 3 |
... # wait for the event to be set await event.wait() |
Once triggered, the task will generate a random number, block for a moment and report a message.
1 2 3 4 5 6 7 |
... # 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}') |
Tying this together, the complete task coroutine is listed below.
1 2 3 4 5 6 7 8 9 10 |
# task coroutine async def task(event, number): # wait for the event to be set await event.wait() # 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}') |
The main coroutine will first create the shared asyncio.Event instance, which will be in the “not set” state by default.
1 2 3 |
... # create a shared event object event = asyncio.Event() |
Next, we can create and start five new coroutines specifying the task() coroutine with the event object and a unique integer as arguments.
1 2 3 |
... # create and run the tasks tasks = [asyncio.create_task(task(event, i)) for i in range(5)] |
Finally, the main coroutine will block for a moment, then trigger the processing in all of the coroutines via the event object.
1 2 3 4 5 6 |
... print('Main blocking...') await asyncio.sleep(0) # start processing in all tasks print('Main setting the event') event.set() |
Finally, the main coroutine will block and wait for all tasks to complete via the asyncio.wait() function.
1 2 3 |
... # await for all tasks to terminate _ = await asyncio.wait(tasks) |
You can learn more about the asyncio.wait() function in the tutorial:
Tying this all together, 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 28 29 30 31 32 33 |
# SuperFastPython.com # example of using an asyncio event object from random import random import asyncio # task coroutine async def task(event, number): # wait for the event to be set await event.wait() # 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 a shared event object event = asyncio.Event() # create and run the tasks tasks = [asyncio.create_task(task(event, i)) for i in range(5)] # allow the tasks to start print('Main blocking...') await asyncio.sleep(0) # start processing in all tasks print('Main setting the event') event.set() # await for all tasks to terminate _ = await asyncio.wait(tasks) # run the asyncio program asyncio.run(main()) |
Running the example first creates the main() coroutine and uses it as the entry point into the asyncio program.
The main() coroutine runs and creates and schedules five task coroutines.
It then sleeps, suspending and allowing the tasks to run and start waiting on the event.
The main coroutine resumes, reports a message then sets the event to True. It then blocks and waits for all issued tasks to complete.
This triggers all five coroutines. They resume in turn perform their processing and report a message.
Note, results will vary each time the program is run given the use of random numbers.
This highlights how coroutines can wait for an event to be set and how we can notify coroutines using an event.
1 2 3 4 5 6 7 |
Main blocking... Main setting the event Task 3 got 0.36705703414223256 Task 1 got 0.4852630342496812 Task 0 got 0.7251916806567016 Task 4 got 0.8104350284043036 Task 2 got 0.9726611709531982 |
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 an asyncio event in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Oli Woodman on Unsplash
Do you have any questions?