How to Use an Asyncio Event in Python
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.
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.
...
# 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:
...
# 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:
...
# set the event
event.set()
The event can be marked as "not set" (whether it is currently set or not) via the clear() function.
...
# 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.
...
# 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.
# task coroutine
async def task(event, number):
# ...
Next, the function will wait for the event to be set before starting the processing work.
...
# 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.
...
# 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.
# 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.
...
# 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.
...
# 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.
...
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.
...
# 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.
# 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.
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
Takeaways
You now know how to use an asyncio event in Python.
If you enjoyed this tutorial, you will love my book: Python Asyncio Jump-Start. It covers everything you need to master the topic with hands-on examples and clear explanations.