Python Asyncio Jump-Start
Python Asyncio Jump-Start
Jason Brownlee
SuperFastPython.com
2022
“I enjoy your postings and intuitive writeups - keep up the good work”
“Great as always!!!”
“Thank you for sharing your knowledge. Your tutorials are one of the
best I’ve read in years. Unfortunately, most authors, try to prove how
clever they are and fail to educate. Yours are very much different. I love
the simplicity of the examples on which more complex scenarios can be
built on, but, the most important aspect in my opinion, they are easy to
understand. Thank you again for all the time and effort spent on
creating these tutorials.”
“Thanks for putting out excellent content Jason Brownlee, tis much
appreciated”
“Wish I had this tutorial 7 yrs ago when I did my first multithreading
software. Awesome Jason”
“This is awesome”
Disclaimer
The author has made every effort to ensure the accuracy of the information
within this book was correct at time of publication. The author does not
assume and hereby disclaims any liability to any party for any loss, damage,
or disruption caused by errors or omissions, whether such errors or omissions
result from accident, negligence, or any other cause.
This guide was carefully designed to help Python developers (like you) to get
productive with asyncio as fast as possible. After completing all seven
lessons, you will know how to bring coroutine-based concurrency with the
asyncio module API to your own projects, super fast.
Together, we can make Python code run faster and change the community’s
opinions about Python concurrency.
This book provides a jump-start guide to get you productive with developing
asyncio programs.
Before we dive into the lessons, let’s look at what is coming with a
breakdown of this book.
Who Is This For
Before we dive in, let’s make sure you’re in the right place.
This book is designed for Python developers who want to discover how to
use and get the most out of the asyncio module to write fast programs.
This book does not require that you are an expert in the Python programming
language or concurrency.
Specifically:
As such, it is not exhaustive. There are many topics that are interesting or
helpful but are not on the critical path to getting you productive fast.
The body of the lesson will introduce a topic with code examples, whereas
the lesson overview will review what was learned with exercises and links for
further information.
Each lesson is also designed to be self-contained so that you can read the
lessons out of order if you choose, such as dipping into topics in the future to
solve specific programming problems.
The lessons were written with some intentional repetition of key concepts.
These gentle reminders are designed to help embed the common usage
patterns in your mind so that they become second nature.
We Python developers learn best from real and working code examples.
Next, let’s learn more about the code examples provided in the book.
Code Examples
All code examples use Python 3.
Python 2.7 is not supported because it reached its end of life in 2020.
I recommend the most recent version of Python 3 available at the time you
are reading this, although Python 3.9 or higher is sufficient to run all code
examples in this book.
1. Save the code file to a directory of your choice with a .py extension.
2. Open your command line (also called the command prompt or terminal).
3. Change the directory to the location where you saved the Python script.
4. Execute the script using the Python interpreter followed by the name of
the script.
For example:
python my_script.py
That being said, if you know what you’re doing, you can run code examples
within your IDE or a notebook if you like. Editors like Sublime Text and
Atom will let you run Python scripts directly, and this is fine. I just can’t help
you debug any issues you might encounter because they’re probably caused
by your development environment.
All lessons in this book provide code examples. These are typically
introduced first via snippets of code that begin with an ellipsis (...) to
clearly indicate that they are not a complete code example. After the program
is introduced via snippets, a complete code example is always listed that
includes all of the snippets tied together, with any additional glue code and
import statements.
I recommend typing code examples from scratch to help you learn and
memorize the API.
Beware of copy-pasting code from the EBook version of this book as you
may accidentally lose or add white space, which may break the execution of
the script.
A code file is provided for each complete example in the book organized by
lesson and example within each lesson. You can execute these scripts directly
or use them as a reference.
APIs can change over time, functions can become deprecated, and idioms can
change and be replaced. I keep this book up to date with changes to the
Python standard library and you can email me any time to get the latest
version. Nevertheless, if you encounter any warnings or problems with the
code, please contact me immediately and I will fix it. I pride myself on
having complete and working code examples in all of my lessons.
Next, let’s learn more about the exercises provided in each lesson.
Practice Exercises
Each lesson has an exercise.
The exercises are carefully designed to test that you understood the learning
outcome of the lesson.
This can be done easily using social media sites like Twitter, Facebook, and
LinkedIn, on a personal blog, or in a GitHub project. Include the name of this
book or SuperFastPython.com to give context to your followers.
You can email me the link to your answer for each exercise directly via:
Next, let’s consider how we might approach working through this book.
How to Read
You can work at your own pace.
This book is designed to be read linearly from start to finish, guiding you
from being a Python developer at the start of the book to being a Python
developer that can confidently use the asyncio module in your project by the
end of the book.
I recommend maintaining a directory with all of the code you type from the
lessons in the book. This will allow you to use the directory as your own
private code library, allowing you to copy-paste code into your projects in the
future.
I recommend trying to adapt and extend the examples in the lessons. Play
with them. Break them. This will help you learn more about how the API
works and why we follow specific usage patterns.
Next, let’s review your newfound capabilities after completing this book.
Learning Outcomes
This book will transform you from a Python developer into a Python
developer that can confidently bring concurrency to your projects with
asyncio.
After working through all of the lessons in this book, you will know:
How to define, create, and run coroutines and how to use the
async/await expressions.
How to create asynchronous tasks, query their status, cancel them and
add callback functions.
How to run many coroutines concurrently in a group and handle their
results.
How to wait for many coroutines to complete, meet a condition, or
timeout.
How to define, create and use asynchronous iterators, generators, and
context managers.
How to use the async for and async with expressions in asyncio
programs.
How to synchronize and coordinate coroutines with locks, semaphores,
events and condition variables.
How to share data between coroutines using coroutine-safe queues.
How to run, read, and write from subprocesses and streams with
coroutines.
How to develop a concurrent and dynamically updating port scanner
using non-blocking I/O.
Next, let’s discover how we can get help when working through the book.
Getting Help
The lessons in this book were designed to be easy to read and follow.
A list of further reading resources is provided at the end of each lesson. These
can be helpful if you are interested in learning more about the topic covered,
such as fine-grained details of the standard library and API functions used.
The conclusions at the end of the book provide a complete list of websites
and books that can help if you want to learn more about Python concurrency
and the relevant parts of the Python standard library. It also lists places where
you can go online and ask questions about Python concurrency.
Finally, if you ever have questions about the lessons or code in this book, you
can contact me any time and I will do my best to help. My contact details are
provided at the end of the book.
Next
Next up in the first lesson, we will discover coroutine-based concurrency
with asyncio in Python.
Lesson 01: Asyncio Concurrency
In this lesson, we will explore asyncio, including coroutines, asynchronous
programming and the asyncio module. We will develop an asyncio hello
world program and understand how it works.
Specifically, coroutines have control over when exactly they suspend their
execution.
Many coroutines can be created and executed at the same time. They have
control over when they will suspend and resume, allowing them to cooperate
as to when concurrent tasks are executed.
Now that we have some idea of what a coroutine is, let’s deepen this
understanding by comparing them to other familiar programming constructs.
The main difference is that a coroutine chooses to suspend and resume its
execution many times before returning and exiting.
Coroutine vs Generator
A generator is a special function that can suspend its execution.
Before coroutines were developed, generators were extended so that they can
be used like coroutines in our programs.
This was made possible via changes to the generators and the introduction of
the yield from expression.
Each program is a process and has at least one thread that executes
instructions for that process.
When we run a script, it starts an instance of the interpreter that runs our code
in the main thread. The main thread is the default thread of a process.
The underlying operating system controls how new threads are created, when
threads are executed, and which CPU core executes them.
This means that coroutines are typically faster to create and start executing
and take up less memory. Conversely, threads are slower than coroutines to
create and start and take up more memory.
Coroutines execute within one thread, therefore a single thread may execute
many coroutines.
What is Asynchronous
This will issue the request to make the function call and will not wait around
for the call to complete. We can choose to check on the status or result of the
function call later.
The function call will happen somehow and at some time, in the background,
and the program can perform other tasks or respond to other events.
This is key. We don’t have control over how or when the request is handled,
only that we would like it handled while the program does other things.
Non-blocking I/O is a way of performing I/O where reads and writes are
requested, although performed asynchronously. The caller does not need to
wait for the operation to complete before returning.
The read and write operations are performed somehow (e.g. by the
underlying operating system or systems built upon it), and the status of the
action and/or data is retrieved by the caller later, once available, or when the
caller is ready.
It is implemented using coroutines that run in an event loop that itself runs in
a single thread.
More broadly, Python offers threads and processes that can execute tasks
asynchronously.
For example, one thread can start a second thread to execute a function call
and resume other activities. The operating system will schedule and execute
the second thread at some time and the first thread may or may not check on
the status of the task, manually.
These classes use the same interface and support asynchronous tasks via the
submit() method that returns a Future object.
For example, one may issue a one-off function call synchronously via the
apply() method or asynchronously via the apply_async() method.
Coroutines in Python
Python supports coroutines directly via additions to the language, including
new expressions and types.
For example:
# define a coroutine
async def custom_coro():
# ...
For example:
# define a coroutine
async def custom_coro():
# await another coroutine
await another_coro()
This was just a taste of coroutines in Python, we will learn more about how to
define, create and run coroutines in Lesson 02: Coroutines and Tasks,
The module provides utility functions and classes to assist in creating and
managing asynchronous tasks and performing non-blocking I/O with sockets
and subprocesses.
Coroutines can be defined and created, but they can only be executed within
an event loop. The event loop that executes coroutines, manages the
cooperative multitasking between coroutines. It is also responsible for
executing callback functions and managing the non-blocking network I/O.
This function takes one coroutine and returns the value of the coroutine. The
provided coroutine can be used as the entry point into the coroutine-based
program. I like to call it the main coroutine, similar to the main thread and
main process in other forms of concurrency.
For example:
...
# start an asyncio program
asyncio.run(main())
Now that we have seen how Python supports coroutines and the asyncio
module supports developing coroutine-based asynchronous programming,
let’s look at a worked example.
Asyncio Hello World Example
The first program we write in a new programming language is a hello world
program.
It is the reason why so many developers are excited to use it, and why others
are afraid to get started.
The first step into asyncio is to write a hello world. The second step is to
understand what it does.
# define a coroutine
async def custom_coroutine():
# report a message
print('Hello world')
Now, let’s slow everything down and understand what this example does.
We can define a coroutine just like a normal function, except it has the added
async keyword before the def keyword.
For example:
# define a coroutine
async def custom_coroutine():
# report a message
print('Hello world')
So far, so good.
For example:
# SuperFastPython.com
# example of calling a coroutine directly
# define a coroutine
async def custom_coroutine():
# report a message
print('Hello world')
Instead, we must have the routine called for us by the asyncio run time, called
the event loop.
For example:
...
# execute the coroutine
asyncio.run(custom_coroutine())
This will start the event loop and execute the coroutine and print the message.
For example:
...
# create the coroutine and assign it to a variable
coro = custom_coroutine()
# execute the coroutine
asyncio.run(coro)
We can clearly see the creation of the coroutine and it is passed to the
asyncio.run() function for execution.
This means that if we create and assign an instance of our custom coroutine
and do not pass it to asyncio.run(), a warning message is reported.
For example:
# SuperFastPython.com
# example of creating but not awaiting a coroutine
# define a coroutine
async def custom_coroutine():
# report a message
print('Hello world')
Running the example creates the coroutine, but does not do anything with it.
The runtime then reports this as a warning message, similar to what we saw
when we called the coroutine directly.
sys:1: RuntimeWarning: coroutine '...' was never awaited
So now we know what our hello world example does. We will go into more
detail in later lessons.
Now that we have seen our first coroutine and asyncio program, let’s look at
when we should use asyncio, and when we shouldn’t.
When to Use Asyncio
There are perhaps three top reasons to adopt asyncio in a project, they are:
Although this is the focus of the asyncio module, it is not the only reason to
adopt it in a project.
Coroutines and asyncio unlock this capability in the Python interpreter and
standard library, with no third-party libraries needed.
A mistake is to think that asyncio is going to make the program faster than
thread or processes, or that coroutines cannot suffer race conditions or
deadlocks. Both of these notions are false.
Coroutines can make code easier to read. The coroutine definitions look and
read like functions. But the challenges of good design and safety in
concurrent programming do not go just by changing the unit of concurrency,
we described previously.
Exercise
Your task for this lesson is to take what you have learned about asyncio and
think about where you could use it to improve the performance of your
programs.
List at least three examples of programs you have worked on recently that
could benefit from the concurrency provided by coroutines and the asyncio
module. No need to share sensitive details of the project or technical details
on how exactly asyncio could be used, just a one or two line high-level
description is sufficient.
If you have trouble coming up with examples of recent applications that may
benefit from using asyncio, then think of applications or functionality you
could develop for current or future projects that could make good use of
asynchronous programming.
This is a useful exercise, as it will start to train your brain to see when you
can and cannot make use of these techniques in practice.
Share your results online on Twitter, LinkedIn, GitHub, or similar.
Send me the link to your results, I’d love to see what you come up with.
Further Reading
This section provides resources for you to learn more about the topics
covered in this lesson.
Next
In the next lesson, we will explore more about how to define coroutines and
how to create, schedule and query asynchronous tasks.
Lesson 02: Coroutines and Tasks
In this lesson, we will explore how to create and use coroutines and tasks in
asyncio programs. These are the primary units of concurrency in asyncio
programs that can be suspended, resumed, canceled and more.
How to define, create, and run a coroutine with the async and await
expressions.
How to create and schedule a coroutine as an independent task.
How to query the status of asyncio tasks, get results, and check for
exceptions.
How to cancel asyncio tasks and add done callback functions.
For example:
# define a coroutine
async def custom_coro():
# ...
A coroutine function can take arguments and return a value, just like a regular
function.
Another key difference is that it can use special coroutine expressions, such
as await, async for, and async with.
We will look at the await expression soon in a following section. We will
learn about the async for, and async with expressions in Lesson 04:
Iterators, Generators, and Context Managers.
For example:
...
# create a coroutine
coro = custom_coro()
For example:
# SuperFastPython.com
# example of checking the type of a coroutine
# define a coroutine
async def custom_coro():
# do nothing
pass
Running the example reports that the created coroutine is a coroutine class.
We also get a RuntimeError because the coroutine was created but never
executed, we will explore that in the next section.
<class 'coroutine'>
sys:1: RuntimeWarning: coroutine '...' was never awaited
The typical way to start a coroutine event loop is via the asyncio.run()
function.
This function takes one coroutine and returns the value of the coroutine. The
provided coroutine can be used as the entry point into the coroutine-based
program.
For example:
...
# create the coroutine
coro = custom_coroutine()
# start the event loop and execute the coroutine
asyncio.run(coro)
This will start the asyncio runtime, called the event loop, and execute the
coroutine.
For example:
# SuperFastPython.com
# example of running a coroutine
import asyncio
# main coroutine
async def main():
# report a message
print('Hello from a coroutine')
Running the example creates the main() coroutine and passes the coroutine
object to the run() function.
The run() function then starts the asyncio event loop and schedules the
main() coroutine.
The await expression takes an awaitable and suspends the caller. It schedules
the provided awaitable if needed. The caller will resume only once the
provided awaitable is done.
For example:
...
# await an awaitable
await coro
For example:
# defines a custom coroutine
def another_coroutine():
...
# create the coroutine
coro = custom_coroutine()
# execute and wait for the coroutine to finish
await coro
For example:
...
# execute and wait for the coroutine to finish
await custom_coroutine()
The asyncio API provides coroutine functions that return coroutine objects
that can be awaited.
For example:
...
# execute and wait for the coroutine to finish
await asyncio.sleep(1)
This suspends the caller and executes a coroutine that sleeps for a given
number of seconds, e.g. is also suspended.
The example below executes a coroutine that in turn executes and awaits the
asyncio.sleep() coroutine.
# SuperFastPython.com
# example of running a coroutine from a coroutine
import asyncio
# main coroutine
async def main():
# report a message
print('Hello from a coroutine')
# sleep for a moment
await asyncio.sleep(1)
Running the example creates the main() coroutine and passes the coroutine
object to the run() function.
The run() function then starts the asyncio event loop and schedules the
main() coroutine.
The main() coroutine executes and reports a message. It then creates the
sleep() coroutine and schedules it for execution, suspending it until it is
done.
The sleep() coroutine runs and suspends itself for a fixed number of
seconds.
The sleep() coroutine terminates, then the main() coroutine resumes and
terminates.
The benefit is that the task provides a handle on the coroutine that can be
queried, from which results can be retrieved, and provides a way to cancel a
running coroutine.
In this section, we will explore how to create and run tasks in asyncio
programs.
Classes that extend the Future class are often referred to as Future-like.
Because a Task is awaitable it means that a coroutine can wait for a task to be
done using the await expression.
For example:
...
# wait for a task to be done
await task
Now that we know what an asyncio task is, let’s look at how we can create
one.
For example:
...
# create a coroutine
coro = task_coroutine()
# create a task from a coroutine
task = asyncio.create_task(coro)
For example:
...
# create a task from a coroutine
task = asyncio.create_task(task_coroutine())
The task instance can be discarded, interacted with via methods, and awaited
by a coroutine.
There are other ways to create a task using the low-level API, but this is the
preferred way to create a Task from a coroutine in an asyncio program.
In fact, the task will not execute until the event loop has an opportunity to
execute it.
This will not happen until all other coroutines are not running and it is the
task’s turn to run.
For example, if we had an asyncio program with one coroutine that created
and scheduled a task, the scheduled task will not run until the calling
coroutine that created the task is suspended.
This may happen if the calling coroutine chooses to sleep, chooses to await
another coroutine or task, or chooses to await the new task that was
scheduled.
For example:
...
# create a task from a coroutine
task = asyncio.create_task(task_coroutine())
# await the task, allowing it to run
await task
Now that we know what a task is and how to create one, let’s look at a
worked example.
# custom coroutine
async def main():
# report a message
print('main coroutine')
# create and schedule the task
task = asyncio.create_task(task_coroutine())
# wait for the task to complete
await task
# start the asyncio program
asyncio.run(main())
Running the example first creates the main() coroutine and uses it as the
entry point to the asyncio program.
The main() coroutine then continues on and then suspends execution and
awaits the task to be completed.
This gives the task an opportunity to execute. It reports a message and then
suspends, sleeping for one second.
At this time both the main() coroutine and the Task are suspended.
The main() coroutine then continues on and terminates, which closes the
asyncio program.
main coroutine
executing the task
Now that we know how to create tasks, let’s look at how we can interact with
them.
How to Use Asyncio Tasks
The asyncio.Task object provides a handle on a scheduled coroutine.
In this section, we will explore how to use asyncio.Task objects once they
have been created.
For example:
...
# check if a task is done
if task.done():
# ...
A task is done if it has had the opportunity to run and is now no longer
running.
A task that has been scheduled is not done. Similarly, a task that is running is
not done.
The method returns True if the task was canceled, or False otherwise.
For example:
...
# check if a task was canceled
if task.cancelled():
# ...
A task is canceled if the cancel() method was called on the task and
completed successfully, e..g cancel() returned True.
A task is not canceled if the cancel() method was not called, or if the
cancel() method was called but failed to cancel the task.
Next, let’s look at how we can get the result from a task.
For example:
...
# get the return value from the wrapped coroutine
value = await task
Another approach to get the result of a task via the result() method.
This returns the return value of the coroutine wrapped by the Task or None if
the wrapped coroutine does not explicitly return a value.
For example:
...
# get the return value from the wrapped coroutine
value = task.result()
For example:
...
try:
# get the return value from the wrapped coroutine
value = task.result()
except Exception:
# task failed and there is no result
For example:
...
try:
# get the return value from the wrapped coroutine
value = task.result()
except asyncio.CancelledError:
# task was canceled
For example:
...
# check if the task was not canceled
if not task.cancelled():
# get the return value from the wrapped coroutine
value = task.result()
else:
# task was canceled
For example:
...
try:
# get the return value from the wrapped coroutine
value = task.result()
except asyncio.InvalidStateError:
# task is not yet done
For example:
...
# check if the task is not done
if not task.done():
await task
# get the return value from the wrapped coroutine
value = task.result()
Next, let’s look at how we can get an unhandled exception raised by a task.
The cancel method returns True if the task was canceled, or False otherwise.
For example:
...
# cancel the task
was_cancelled = task.cancel()
If the task is already done, it cannot be canceled and the cancel() method
will return False and the task will not have the status of canceled.
If cancel request was successfully, the next time the task is given an
opportunity to run, it will raise a CancelledError exception.
The cancel() method can also take a message argument which will be used
in the content of the CancelledError.
In this example, we will create and schedule a task as per normal. The caller
will then wait a moment and allow the task to begin executing.
It will then cancel the task and check that the request to cancel was
successful.
The caller will then wait a moment more for the task to be canceled, then
report the status of the task to confirm it is marked as canceled.
# custom coroutine
async def main():
# report a message
print('main coroutine')
# create and schedule the task
task = asyncio.create_task(task_coroutine())
# wait a moment
await asyncio.sleep(0.5)
# cancel the task
was_cancelled = task.cancel()
print(f'>was canceled: {was_cancelled}')
# wait a moment
await asyncio.sleep(0.1)
# report the status
print(f'>canceled: {task.cancelled()}')
Running the example first creates the main() coroutine and uses it as the
entry point to the asyncio program.
The main() coroutine then suspends execution and suspends for half a
second.
This gives the task an opportunity to execute the task. The task reports a
message and then suspends, sleeping for one second.
The main() coroutine resumes and cancels the new task. It then reports
whether the request to cancel the task was successful. It was because we
know that the task is not yet done. The main() coroutine then suspends for a
fraction of a second.
This gives the task another opportunity to execute, in which case the
CancelledError exception is raised in the wrapped coroutine, canceling the
task.
The main() coroutine then resumes and checks the canceled status of the
task, confirming that it indeed is done and was canceled.
main coroutine
executing the task
>was canceled: True
>canceled: True
Next, let’s look at how we can add a done callback function to a task.
How to Use Callback With a Task
This method takes the name of a function to call when the task is done.
For example:
# done callback function
def handle(task):
print(task)
...
# register a done callback function
task.add_done_callback(handle)
Recall that a task may be done when the wrapped coroutine finishes normally
when it returns, when an unhandled exception is raised or when the task is
canceled.
For example:
...
# remove a done callback function
task.remove_done_callback(handle)
In this example, we will define a done callback function that will report
whether a task is done or not.
The function will then be registered on the task after it is created.
# custom coroutine
async def main():
# report a message
print('main coroutine')
# create and schedule the task
task = asyncio.create_task(task_coroutine())
# add a done callback function
task.add_done_callback(handle)
# wait for the task to complete
await task
Running the example first creates the main() coroutine and uses it as the
entry point to the asyncio program.
The main() coroutine then registers the done callback function on the task. It
then suspends execution and awaits the task to be completed.
This gives the task an opportunity to execute. It reports a message and then
suspends, sleeping for one second. It resumes and terminates.
This triggers the asyncio infrastructure to call the callback function and pass
it the Task instance.
This name can be helpful if multiple tasks are created from the same
coroutine and we need some way to tell them apart programmatically.
The name can be set when the task is created from a coroutine via the name
argument.
For example:
...
# create a coroutine
coro = task_coroutine()
# create a task from a coroutine
task = asyncio.create_task(coro, name='MyTask')
The name for the task can also be set via the set_name() method.
For example:
...
# set the name of the task
task.set_name('MyTask')
For example:
...
# get the name of a task
name = task.get_name()
This is just a sample of some of the key ways that we can interact with an
asyncio.Task that represents a scheduled coroutine.
Lesson Review
Takeaways
You now know how to define, create, and run a coroutine with the async
and await expressions.
You now know how to create and schedule a coroutine as an
independent task.
You now know how to query the status of asyncio tasks, get results, and
check for exceptions.
You now know how to cancel asyncio tasks and add done callback
functions.
Exercise
Your task for this lesson is to use what you have just learned about running
ad hoc code in a coroutine or independent task.
The specifics of the task do not matter. You can try to complete something
practical, or if you run out of ideas, you can calculate a number, or suspend
with the asyncio.sleep() function.
Now update the program to execute each task using a separate coroutines
executed concurrently as independent asyncio.Tasks. Record how long it
takes to execute.
Compare the execution time between the serial and concurrent versions of the
program. Calculate the difference in seconds (e.g. it is faster by 5 seconds).
Calculate the ratio that the second program is faster than the first program
(e.g. it is 2.5x faster).
These calculations may help:
If it turns out that the asynchronous version of the program is not faster,
perhaps change or manipulate the task so that the serial version is slower than
the faster version.
This exercise will help you develop the calculations and practice needed to
benchmark and compare the performance before and after making code
concurrent with asyncio.
Send me the link to your results, I’d love to see what you come up with.
Further Reading
This section provides resources for you to learn more about the topics
covered in this lesson.
Next
In the next lesson, we will explore how to run multiple coroutines as a group
and wait for groups of coroutines to complete or meet a condition.
Lesson 03: Collections of Tasks
In this lesson, we will explore how to run and work with collections of
coroutines and asyncio tasks. This is the main way to issue and wait on many
asynchronously issued and concurrently executing tasks at points in our
program.
This is a likely situation where the result is required from many similar tasks,
e.g. same task or coroutine with different data.
The awaitables can be executed concurrently, results returned, and the main
program can resume by making use of the results.
Now that we know what the asyncio.gather() function is, let’s look at how
we can use it.
For example:
...
# execute multiple coroutines
asyncio.gather(coro1(), coro2())
For example:
...
# create a list of coroutines
coros = [coro1(), coro2()]
# cannot provide a list of awaitables directly
asyncio.gather(corors) # error
For example:
...
# create a list of coroutines
coros = [coro1(), coro2()]
# gather with an unpacked list of awaitables
asyncio.gather(*coros)
For example:
...
# get a future that represents multiple awaitables
group = asyncio.gather(coro1(), coro2())
This will collect the return values from the coroutines and tasks and return
them as an iterable.
For example:
...
# execute coroutines and get the return values
values = await asyncio.gather(coro1(), coro2())
We can collect many coroutines together into a list either manually or using a
list comprehension.
For example:
...
# create many coroutines
coros = [task_coro(i) for i in range(10)]
This can be achieved by unwrapping the list into separate expressions and
passing them to the gather() function. The star operator (*) will perform this
operation for us.
For example:
...
# run the tasks
await asyncio.gather(*coros)
Running the example executes the main() coroutine as the entry point to the
program.
The main() coroutine then creates a list of 10 coroutine objects using a list
comprehension.
This list is then provided to the gather() function and unpacked into 10
separate expressions using the star operator.
The main() coroutine then awaits the Future object returned from the call to
gather(), suspending and waiting for all scheduled coroutines to complete
their execution.
The coroutines run as soon as they are able, reporting their unique messages
and sleeping before terminating.
Only after all coroutines in the group are complete does the main() coroutine
resume and report its final message.
Next, let’s look at how we can wait for many tasks in an asyncio program.
How to Wait for Many Tasks
The asyncio.wait() function can be used to wait for a collection of asyncio
tasks to complete.
The call to wait can be configured to wait for different conditions, such as all
tasks being completed, the first task completed and the first task failing with
an error.
This could be a list, dict, or set of task objects that we have created, such
as via calls to the asyncio.create() task function in a list comprehension.
For example:
...
# create many tasks
tsks = [asyncio.create_task(task(i)) for i in range(10)]
The asyncio.wait() will not return until some condition on the collection of
tasks is met.
These sets are referred to as the done set and the pending set.
For example:
...
# wait for all tasks to complete
done, pending = await asyncio.wait(tasks)
We can then await this coroutine which will return the tuple of sets.
For example:
...
# create the wait coroutine
wait_coro = asyncio.wait(tasks)
# await the wait coroutine
t = await wait_coro
For example:
...
# wait for all tasks to complete
done, pending = await asyncio.wait(tasks,
return_when=asyncio.ALL_COMPLETED)
For example:
...
# wait for the first task to be completed
done, pending = await asyncio.wait(tasks,
return_when=asyncio.FIRST_COMPLETED)
When the first task is complete and returned in the done set, the remaining
tasks are not canceled and continue to execute concurrently.
We can wait for the first task to fail with an exception by setting
return_when to FIRST_EXCEPTION.
For example:
...
# wait for the first task to fail
done, pending = await asyncio.wait(tasks,
return_when=asyncio.FIRST_EXCEPTION)
In this case, the done set will contain the first task that failed with an
exception. If no task fails with an exception, the done set will contain all
tasks and wait() will return only after all tasks are completed.
We can specify how long we are willing to wait for the given condition via a
timeout argument in seconds.
If the timeout expires before the condition is met, the tuple of tasks is
returned with whatever subset of tasks do meet the condition at that time,
e.g. the subset of tasks that are completed if waiting for all tasks to complete.
For example:
...
# wait for all tasks to complete with a timeout
done, pending = await asyncio.wait(tasks, timeout=3)
Now that we know how to use the asyncio.wait() function, let’s look at a
worked example.
Example of Waiting for All Tasks
The main coroutine will then create many tasks in a list comprehension with
the coroutine and then wait for all tasks to be complete.
# main coroutine
async def main():
# create many tasks
tasks = [asyncio.create_task(task_coro(i))
for i in range(10)]
# wait for all tasks to complete
_ = await asyncio.wait(tasks)
# report results
print('All done')
The main() coroutine then creates a list of ten tasks in a list comprehension,
each providing a unique integer argument from 0 to 9.
The main() coroutine is then suspended and waits for all tasks to complete.
The tasks execute. Each generates a random value, sleeps for a moment, then
reports its generated value.
After all tasks have been completed, the main() coroutine resumes and
reports a final message.
This example highlights how we can use the wait() function to wait for a
collection of tasks to be completed.
NOTE: Results will vary each time the program is run given the use of
random numbers.
>task 5 done with 0.0591009105682192
>task 8 done with 0.10453715687017351
>task 0 done with 0.15462838864295925
>task 6 done with 0.4103492027393125
>task 9 done with 0.45567100006991623
>task 2 done with 0.6984682905809402
>task 7 done with 0.7785363531316224
>task 3 done with 0.827386088873161
>task 4 done with 0.9481344994700972
>task 1 done with 0.9577302665040541
All done
Next, let’s look at how we can wait for a coroutine to complete with a fixed
timeout.
How to Wait For a Task With a Timeout
The asyncio.wait_for() function allows the caller to wait for an asyncio
task or coroutine to complete with a timeout.
If no timeout is specified, the wait_for() function will wait until the task is
completed.
If a timeout is specified and elapses before the task is complete, then the task
is canceled.
This allows the caller to both set an expectation about how long they are
willing to wait for a task to complete, and to enforce the timeout by canceling
the task if the timeout elapses.
Now that we know what the asyncio.wait_for() function is, let’s look at
how to use it.
For example:
...
# wait for a task to complete
await asyncio.wait_for(coro, timeout=10)
If the timeout elapses before the task is completed, the task is canceled, and
an asyncio.TimeoutError is raised, which may need to be handled.
For example:
...
# execute a task with a timeout
try:
# wait for a task to complete
await asyncio.wait_for(coro, timeout=1)
except asyncio.TimeoutError:
# ...
If the waited-for task fails with an unhandled exception, the exception will be
propagated back to the caller that is awaiting on the wait_for() coroutine, in
which case it may need to be handled.
For example
...
# execute a task that may fail
try:
# wait for a task to complete
await asyncio.wait_for(coro, timeout=1)
except asyncio.TimeoutError:
# ...
except Exception:
# ...
Now that we know how the wait_for() function works, let’s look at a
worked example.
The task coroutine is modified so that it sleeps for more than one second,
ensuring that the timeout always expires before the task is complete.
# main coroutine
async def main():
# create a task
task = task_coro()
# execute and wait for the task without a timeout
try:
await asyncio.wait_for(task, timeout=0.2)
except asyncio.TimeoutError:
print('Gave up waiting, task canceled')
Running the example first creates the main() coroutine and uses it as the
entry point into the asyncio program.
The main() coroutine creates the task coroutine. It then calls wait_for() and
passes the task coroutine and sets the timeout to 0.2 seconds.
The main() coroutine resumes after the timeout has elapsed. The wait_for()
coroutine cancels the task_coro() coroutine and the main() coroutine is
suspended.
The main() coroutine resumes and handles the TimeoutError raised by the
task_coro().
This highlights how we can call the wait_for() function with a timeout and
to cancel a task if it is not completed within a timeout.
NOTE: Results will vary each time the program is run given the use of
random numbers.
>task got 1.4231068884240894
Gave up waiting, task canceled
Next, let’s look at how we can handle coroutine results in the order they are
completed.
How to Handle Tasks In Completion Order
The asyncio.as_completed() function will run a collection of tasks and
coroutines concurrently.
Now that we know what as_completed() is, let’s look at how we can use it.
This may be a list, dict, or set, and may contain asyncio.Task objects,
coroutines, or other awaitables.
It returns an iterable that when traversed will yield awaitables in the provided
list. These can be awaited by the caller in order to get results in the order that
tasks are completed, e.g. get the result from the next task to complete.
For example:
...
# iterate over awaitables
for task in asyncio.as_completed(tasks):
# get the next result
result = await task
For example:
...
# iterate over awaitables with a timeout
for task in asyncio.as_completed(tasks, timeout=10):
# get the next result
result = await task
For example:
...
# handle a timeout
try:
# iterate over awaitables with a timeout
for task in asyncio.as_completed(tasks, timeout=10):
# get the next result
result = await task
except asyncio.TimeoutError:
# ...
Now that we know how to use the as_completed() function, let’s take a
moment to consider how it works.
For example:
...
# get a gen that yields awaitables in completion order
generator = asyncio.as_completed(tasks)
Calling the next() built-in function on the generator does not suspend, but
instead yields a coroutine.
The returned coroutine is not one of the provided awaitables, but rather an
internal coroutine from the as_completed() function that manages and
monitors which issued task will return a result next.
For example:
...
# get the next coroutine
coro = next(generator)
It is not until one of the returned coroutines is awaited that the caller will
suspend.
For example:
...
# get a result from the next task to complete
result = await coro
Now that we have an idea of how to use the as_completed() function and
how it works, let’s look at a worked example.
In this example, we will define a simple coroutine task that takes an integer
argument, generates a random value, sleeps for a fraction of a second then
returns the integer argument multiplied by the generated value.
In each iteration of the loop a coroutine is yielded from the generator and is
then awaited in the body of the loop. A result from the next coroutine to
complete is returned and the result is reported.
# main coroutine
async def main():
# create many coroutines
coros = [task_coro(i) for i in range(10)]
# get results as coroutines are completed
for coro in asyncio.as_completed(coros):
# get the result from the next task to complete
result = await coro
# report the result
print(f'>got {result}')
Running the example first creates the main() coroutine and then uses this as
the entry point into the asyncio program.
The tasks begin executing, generating a random value, and sleeping. A task
finishes and returns a value.
The main() coroutine resumes, receives the return value, and reports it.
The loop repeats, another coroutine is yielded, the main() coroutine awaits it
and suspends, and another result is returned.
This continues until all coroutines in the provided list are completed.
NOTE: Results will vary each time the program is run given the use of
random numbers.
>got 0.07236962530232949
>got 0.5171864910147306
>got 0.7153626682953872
>got 2.54812333824902
>got 0.5960648114598495
>got 5.051883987489034
>got 0.0
>got 2.842043340472799
>got 6.343694133393031
>got 4.903128525746293
Next, let’s look at how we can call blocking functions without blocking the
asyncio event loop.
How to Run Blocking Tasks
We can execute thread-blocking function calls in asyncio using the
asyncio.to_thread() function.
It will take a function call and execute it in a new thread, separate from the
thread that is executing the asyncio event loop.
This is a useful function to use when we have an asyncio program that needs
to perform both non-blocking I/O (such as with sockets) and blocking I/O
(such as with files or a legacy API).
It then returns a coroutine that can be awaited to get the return value from the
function, if any.
For example:
...
# create a coroutine for a blocking function
blocking_coro = asyncio.to_thread(blocking, arg1, arg2)
# await the coroutine and get return value
result = await blocking_coro
The blocking function will not be executed in a new thread until it is awaited
or executed independently.
For example:
...
# create a coroutine for a blocking function
blocking_coro = asyncio.to_thread(blocking)
# execute the blocking function independently
task = asyncio.create_task(blocking_coro)
This allows the blocking function call to be used like any other asyncio Task.
Now that we know how to use the asyncio.to_thread() function, let’s look
at a worked example.
This coroutine is then awaited allowing the main coroutine to suspend and for
the blocking function to execute in a new thread.
# blocking function
def blocking_task():
# report a message
print('task is running')
# block the thread
time.sleep(2)
# report a message
print('task is done')
# main coroutine
async def main():
# run the background task
_= asyncio.create_task(background())
# create a coroutine for the blocking function call
coro = asyncio.to_thread(blocking_task)
# make call in a new thread and await the result
await coro
Running the example first creates the main() coroutine and uses it as the
entry point into the asyncio program.
The main() coroutine runs. It creates the background coroutine and schedules
it for execution as soon as it can.
The main() coroutine then creates a coroutine to run the background task in a
new thread and then awaits this coroutine.
Firstly, it suspends the main() coroutine, allowing any other coroutines in the
event loop to run, such as the new coroutine for executing the blocking
function in a new thread.
The new coroutine runs and starts a new thread and executes the blocking
function in the new thread. This coroutine was also suspended.
The event loop is free and the background coroutine gets an opportunity to
run, looping and reporting its messages.
The blocking call in the other thread finishes, suspends the background task,
resumes the main thread, and terminates the program.
This highlights that running a blocking call in a new thread does not block
the event loop, allowing other coroutines to run while the blocking call is
being executed, suspending some threads other than the main event loop
thread.
task is running
>background task running
>background task running
>background task running
>background task running
task is done
Lesson Review
Takeaways
You now know how to run many coroutines concurrently as a group and
retrieve their results.
You now know how to wait for a collection of tasks to complete or for
the first of a group to complete or fail.
You now know how to wait for an asynchronous task to complete with a
timeout.
You now know how to handle task results in the order that tasks are
completed.
You now know how to run blocking function calls asynchronously in a
separate thread.
Exercise
Your task for this lesson is to use what you have learned about running and
waiting for many asynchronous tasks.
Develop a small asyncio program that creates and starts one or more
asynchronous task performing some arbitrary activity, like sleeping.
Then from the main coroutine, wait for some time or a trigger and query the
status one or more of the tasks you have created. For example, you could wait
for all tasks to complete or for a timeout.
Extend the example so that the tasks take an random amount of time to
complete or randomly raise an exception or not. Have the main coroutine
wait for the first task to complete, the first to fail, or report results as they are
completed.
This example will help you get used to working with and operating upon
collections of asynchronous tasks that are executing concurrently.
Share your results online on Twitter, LinkedIn, GitHub, or similar.
Send me the link to your results, I’d love to see what you come up with.
Further Reading
This section provides resources for you to learn more about the topics
covered in this lesson.
Next
In the next lesson, we will explore how to define, create and use
asynchronous iterators, generators, and context managers.
Lesson 04: Iterators, Generators,
and Context Managers
In this lesson, we will explore how to create and to use asynchronous
iterators, generators, and context managers in asyncio programs. These are
the asynchronous versions of the classical iterators, generators, and context
managers we may use in conventional programs.
How to define, create and traverse asynchronous iterators and how they
compare to classical iterators.
How to create and use asynchronous generators and how they compare
to classical generators.
How to define and create asynchronous context managers and how they
compare to classical context managers
How and when to use the async for expression in coroutines with
asynchronous iterables.
How and when to use the async with expression for use with
asynchronous context managers.
Many objects are iterable, most notable are containers such as lists.
For example:
# define an asynchronous iterator
class AsyncIterator():
# constructor, define some state
def __init__(self):
self.counter = 0
For example:
...
# create the iterator
it = AsyncIterator()
For example:
...
# step the async iterator
result = await anext(it)
For example:
...
# traverse an asynchronous iterator
async for result in AsyncIterator():
print(result)
This does not execute the for-loop in parallel. Instead, this is an asynchronous
for-loop.
The difference is that the coroutine that executes the for-loop will suspend
and internally await each awaitable iteration.
Behind the scenes, this may require coroutines to be scheduled and awaited,
or tasks to be awaited.
We may also use an asynchronous list comprehension with the async for
expression to collect the results of the iterator.
For example:
...
# async list comprehension with async iterator
results = [item async for item in AsyncIterator()]
Now that we are familiar with how to create and use asynchronous iterators,
let’s look at a worked example.
This loop will automatically await each awaitable returned from the iterator,
retrieve the returned value, and make it available within the loop body so that
in this case it can be reported.
This is perhaps the most common usage pattern for asynchronous iterators.
# main coroutine
async def main():
# loop over async iterator with async for loop
async for item in AsyncIterator():
print(item)
Running the example first creates the main() coroutine and uses it as the
entry point into the asyncio program.
1
2
3
4
5
6
7
8
9
10
Before we dive into the details of asynchronous generators, let’s first review
classical generators and see how they compare to asynchronous generators.
For example:
# define a generator
def generator():
for i in range(10):
yield i
For example:
...
# create the generator
gen = generator()
# step the generator
result = next(gen)
For example:
...
# traverse the generator and collect results
results = [item for item in generator()]
Unlike a function generator, the coroutine can schedule and await other
coroutines and tasks.
This means that the function is defined using the async def expression.
For example:
# define an asynchronous generator
async def async_generator():
for i in range(10)
yield i
Because the asynchronous generator is a coroutine and each iterator returns
an awaitable that is scheduled and executed in the asyncio event loop, we can
execute and await awaitables within the body of the generator.
For example:
# define an asynchronous generator that awaits
async def async_generator():
for i in range(10)
# suspend and sleep a moment
await asyncio.sleep(1)
# yield a value to the caller
yield i
This looks like calling the generator but instead creates and returns an iterator
object, called an iterable.
For example:
...
# create the iterator
it = async_generator()
For example:
...
# get an awaitable for one step of the generator
awaitable = anext(gen)
# execute the one step of the gen and get the result
result = await awaitable
For example:
...
# step the async generator
result = await anext(gen)
For example:
...
# traverse an asynchronous generator
async for result in async_generator():
print(result)
We may also use an asynchronous list comprehension with the async for
expression to collect the results of the generator.
For example:
...
# async list comprehension with async generator
results = [item async for item in async_generator()]
Now that we know how to create and use asynchronous generators, let’s look
at a worked example.
This loop will automatically await each awaitable returned from the
generator, retrieve the yielded value, and make it available within the loop
body so that in this case it can be reported.
This is perhaps the most common usage pattern for asynchronous generators.
# main coroutine
async def main():
# loop over async generator with async for loop
async for item in async_generator():
print(item)
0
1
2
3
4
5
6
7
8
9
Next, let’s look at how to use asynchronous context managers in our asyncio
programs.
How to Use Asynchronous Context Managers
An asynchronous context manager is an object that implements the
__aenter__() and __aexit__() methods.
The __exit__() method defines what happens when the code block is exited,
such as closing a prepared resource.
For example:
...
# open a context manager
with ContextManager() as manager:
# ...
# closed automatically
This is achieved using the async with expression which we will learn more
about in a moment.
For example:
# define an asynchronous context manager
class AsyncContextManager:
# enter the async context manager
async def __aenter__(self):
# report a message
print('>entering the context manager')
Because each of the methods are coroutines, they may themselves await
coroutines or tasks.
For example:
# define an asynchronous context manager
class AsyncContextManager:
# enter the async context manager
async def __aenter__(self):
# report a message
print('>entering the context manager')
# suspend for a moment
await asyncio.sleep(0.5)
Next, let’s look at how we can create and use an asynchronous context
manager.
It will automatically await the enter and exit coroutines, suspending the
calling coroutine as needed.
For example:
...
# use an asynchronous context manager
async with AsyncContextManager() as manager:
# ...
Now that we know how to use asynchronous context managers, let’s look at a
worked example.
In this example, we will create and use the context manager in a normal
manner.
We will use an async with expression and on one line, create and enter the
context manager. This will automatically await the enter method.
We can then make use of the manager within the inner code block. In this
case, we will just report a message.
Exiting the inner code block will automatically await the exit method of the
context manager.
Running the example first creates the main() coroutine and uses it as the
entry point into the asyncio program.
This expression automatically calls the enter method and awaits the
coroutine. A message is reported and the coroutine suspends for a moment.
The main() coroutine resumes and executes the body of the context manager,
printing a message.
The code block is exited and the exit method of the context manager is
awaited automatically, reporting a message and sleeping a moment.
You now know how to define, create and traverse asynchronous iterators
and how they compare to classical iterators.
You now know how to create and use asynchronous generators and how
they compare to classical generators.
You now know how to define and create asynchronous context
managers and how they compare to classical context managers
You now know how and when to use the async for expression in
coroutines with asynchronous iterables.
You now know how and when to use the async with expression for use
with asynchronous context managers.
Exercise
Your task in this lesson is to use what you have learned about asynchronous
iterators.
Ensure that the coroutine executed each step does some work or simulated
work such as a sleep.
Create and traverse the iterator in the main loop of your asyncio program
using the async for expression.
This will help make you more comfortable with asynchronous loops and
iteration. There is a lot of general confusion about the async for expression
and you will better understand what it is doing by developing your own
asynchronous iterators and generators.
Send me the link to your results, I’d love to see what you come up with.
Further Reading
This section provides resources for you to learn more about the topics
covered in this lesson.
Next
In the next lesson, we will explore how to synchronize and coordinate
coroutines and how to share data between coroutines.
Lesson 05: Queues and
Synchronization Primitives
In this lesson, we will explore how to share data between coroutines using
queues and how to use concurrency primitives to synchronize and coordinate
coroutines in our asyncio programs.
Primarily, it refers to the fact that the code is free of race conditions.
Although two or more coroutines cannot execute at the same time within the
event loop, it is possible for program state and resources to be corrupted or
made inconsistent via concurrent execution.
This lesson will focus of ways to share data, protect data, and coordinate
behavior between coroutines that is coroutine-safe.
How to Share Data Between Coroutines with
Queues
A queue can be used to share data between coroutines.
The asyncio module provides the asyncio.Queue class for general use, but
also provides a last-in-first-out (LIFO) queue via the asyncio.LifoQueue
class and a priority queue via the asyncio.PriorityQueue class.
Coroutine-safe means that it can be used by multiple coroutines to put and get
items concurrently without a race condition.
The Queue class provides a first-in, first-out FIFO queue, which means that
the items are retrieved from the queue in the order they were added. The first
items added to the queue will be the first items retrieved.
For example:
...
# created an unbounded queue
queue = asyncio.Queue()
A maximum capacity can be set on a new Queue via the maxsize constructor
augment.
For example:
...
# created a queue with a maximum capacity
queue = asyncio.Queue(maxsize=100)
Items can be added to the queue via a call to the put() method. This is a
coroutine that must be awaited, suspending the caller until the item can be
placed on the queue successfully. For example:
...
# add an item to the queue
await queue.put(item)
If a size-limited queue becomes full, new items cannot be added and calls to
put() will suspend until space becomes available.
Items can be retrieved from the queue by calls to get(). This is also a
coroutine and must be awaited, suspending the caller until an item can be
retrieved from the queue successfully.
For example:
...
# get an item from the queue
item = await queue.get()
Now that we know how to use queues, let’s look at a worked example of
sharing data between coroutines.
In this example, we will define a custom task function that takes the queue as
an argument, generates some data, and puts that data on the queue. It is a
producer coroutine.
The main coroutine will create the queue, share it with the new coroutine and
then suspend, waiting for data to arrive on the queue. The main coroutine will
be the consumer coroutine.
Running the example first creates the main() coroutine and uses it as the
entry point into the asyncio program.
The main() coroutine runs and the shared asyncio.Queue object is then
created.
Next, the producer coroutine is created and passed the queue instance. Then
the consumer coroutine is started and the main coroutine suspends until both
coroutines terminate.
The producer coroutine generates a new random value for each iteration of
the task, suspends, and adds it to the queue. The consumer coroutine waits on
the queue for items to arrive, then consumes them one at a time, reporting
their value.
Finally, the producer task finishes, a None value is put on the queue and the
coroutine terminates. The consumer coroutine gets the None value, breaks its
loop, and also terminates.
This highlights how the asyncio.Queue can be used to share data easily
between producer and consumer coroutines.
NOTE: Results will vary each time the program is run given the use of
random numbers.
Producer: Running
Consumer: Running
>got 0.7559246569022605
>got 0.965203750033905
>got 0.49834912260024233
>got 0.22783211775499135
>got 0.07775542407106295
>got 0.5997647474647314
>got 0.7236540952500915
>got 0.7956407178426339
>got 0.11256095725867177
Producer: Done
>got 0.9095338767572713
Consumer: Done
Next, let’s look at how we can use mutex locks in our asyncio programs.
How to Protect Critical Sections with a Mutex Lock
A mutual exclusion lock, or mutex lock for short, is a concurrency primitive
intended to prevent a race condition.
A race condition is a concurrency failure case when two coroutines run the
same code and access or update the same resource (e.g. data variables,
stream, etc.) leaving the resource in an unknown and inconsistent state.
An instance of the Lock class can be created and then acquired by coroutines
before accessing a critical section, and released after exiting the critical
section.
The acquire() method is used to acquire the lock. It is a coroutine and must
be awaited, suspending the calling coroutine. The lock can be released again
later via the release() method.
For example:
...
# create a lock
lock = asyncio.Lock()
# acquire the lock
await lock.acquire()
# ...
# release the lock
lock.release()
Only one coroutine can have the lock at any time. If a coroutine does not
release an acquired lock, it cannot be acquired again.
The coroutine attempting to acquire the lock will suspend until the lock is
acquired, such as if another coroutine currently holds the lock then releases it.
We can also use the lock via the context manager interface via the async
with expression, allowing the critical section to be a block of code within the
context manager and for the lock to be released automatically once the block
of code is exited, normally or otherwise.
For example:
...
# create a lock
lock = asyncio.Lock()
# acquire the lock
async with lock:
# ...
This is the preferred usage of the lock as it makes it clear where the protected
code begins and ends, and ensures that the lock is always released, even if
there is an exception or error within the critical section.
Now that we know how to use mutex locks, let’s look at a worked example.
In this example, we will define a target task that takes a lock as an argument
and uses the lock to protect a critical section, which in this case will print a
message and sleep for a moment.
The complete example is listed below.
# SuperFastPython.com
# example of an asyncio mutual exclusion (mutex) lock
from random import random
import asyncio
# entry point
async def main():
# create a shared lock
lock = asyncio.Lock()
# create many concurrent coroutines
coros = [task(lock, i, random()) for i in range(10)]
# execute and wait for tasks to complete
await asyncio.gather(*coros)
Running the example first creates the main() coroutine and uses it as the
entry point into the asyncio program.
It then creates a list of coroutines, each is passed the shared lock, a unique
integer, and a random floating point value.
The list of coroutines is passed to the gather() function and the main()
coroutine suspends until all coroutines are completed.
A task coroutine executes, acquires the lock, reports a message, then awaits
the sleep, suspending.
Another coroutine resumes. It attempts to acquire the lock and is suspended,
while it waits. This process is repeated with many if not all coroutines.
The first coroutines resumes, exits the block of code, and releases the lock
automatically via the asynchronous context manager.
The first coroutine to wait on the lock resumes, acquires the lock, reports a
message, and sleeps.
This process repeats until all coroutines are given an opportunity to acquire
the lock, execute the critical section and terminate.
Once all tasks terminate, the main() coroutine resumes and terminates,
closing the program.
NOTE: Results will vary each time the program is run given the use of
random numbers.
>coroutine 0 got the lock, sleeping for 0.35342849008361
>coroutine 1 got the lock, sleeping for 0.78996044707365
>coroutine 2 got the lock, sleeping for 0.10018104240779
>coroutine 3 got the lock, sleeping for 0.75009875150084
>coroutine 4 got the lock, sleeping for 0.54066805101353
>coroutine 5 got the lock, sleeping for 0.53074317625936
>coroutine 6 got the lock, sleeping for 0.44269144160147
>coroutine 7 got the lock, sleeping for 0.79732810383210
>coroutine 8 got the lock, sleeping for 0.49827720719979
>coroutine 9 got the lock, sleeping for 0.18177356607777
Next, let’s look at how we can use semaphores in our asyncio programs.
How to Limit Access to a Resource with a
Semaphore
A semaphore is a concurrency primitive that allows a limit on the number of
coroutines that can acquire a lock protecting a critical section or resource.
It is an extension of a mutual exclusion (mutex) lock that adds a count for the
number of coroutines that can acquire the lock before additional coroutines
will suspend. Once full, new coroutines can only acquire access on the
semaphore once an existing coroutine holding the semaphore releases access.
The Semaphore object 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.
The semaphore can be acquired by calling the acquire() method which must
be awaited.
For example:
...
# acquire the semaphore
await semaphore.acquire()
By default, the calling coroutine will suspend until access becomes available
on the semaphore.
Once acquired, the semaphore can be released again by calling the release()
method.
For example:
...
# release the semaphore
semaphore.release()
The Semaphore class supports usage via the context manager, which will
automatically acquire and release the semaphore for us. As such it is the
preferred way to use semaphores in our programs.
For example:
...
# acquire the semaphore
async with semaphore:
# ...
Now that we know how to use semaphores, let’s look at a worked example.
In this example, we will start a suite of coroutines but limit the number that
can perform an action simultaneously. A semaphore will be used to limit the
number of concurrent tasks that may execute which will be less than the total
number of coroutines, allowing some coroutines to suspend, wait for access,
then be notified and acquire access.
# task coroutine
async def task(semaphore, number):
# acquire the semaphore
async with semaphore:
# generate a random value between 0 and 1
value = random()
# suspend 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)
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.
Once acquired, a task generates a random value, suspend 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 asyncio.sleep().
This highlights how we can limit the number of coroutines to execute a block
of code concurrently.
NOTE: Results will vary each time the program is run given the use of
random numbers.
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
Next, let’s look at how to use events in asyncio programs.
How to Signal Between Coroutines Using an Event
An event is a coroutine-safe boolean flag that can be used to signal between
two or more coroutines.
It can be useful to coordinate the behavior of many coroutines that can check
the status of the flag, such as to begin processing, or to stop processing and
exit.
An Event class wraps a boolean variable that can either be set (True) or not
set (False). Coroutines sharing the Event object 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.
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() method
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() method. Any coroutines waiting on the
event to be set will be notified.
For example:
...
# set the event
event.set()
Finally, coroutines can wait for the event to be set via the wait() method,
which must be awaited. Calling this method will suspend until the event is
marked as set (e.g. another coroutine calling the set() method). If the event
is already set, the wait() method will return immediately.
...
# wait for the event to be set
await event.wait()
In this example we will create a suite of coroutines that each will perform
some work 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 new coroutines to start work.
# 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()
# suspend 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 suspending...')
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)
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 suspends and waits for all issued tasks to complete.
This triggers all five coroutines. They resume in turn perform their
processing and report a message.
This highlights how coroutines can wait for an event to be set and how we
can notify coroutines using an event.
NOTE: Results will vary each time the program is run given the use of
random numbers.
Main suspending...
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
Next, let’s look at how we can use condition variables in asyncio programs.
How to Coordinate Using a Condition Variable
A condition variable, also called a monitor, allows multiple coroutines to wait
and be notified about some result.
Another coroutine can then acquire the condition, make a change in the
program, and notify one, all, or a subset of coroutines waiting on the
condition that something has changed.
The waiting coroutine can then resume, re-acquire the condition, perform
checks on any changed state and perform required actions.
Next, let’s look at how we can create and use condition variables.
For example:
...
# create a new condition variable
condition = asyncio.Condition()
In order for a coroutine to make use of the Condition, it must acquire it and
release it, like a mutex lock.
This can be achieved manually with the acquire() and release() methods.
The acquire() method is a coroutine and must be awaited.
For example, we can acquire the Condition and then wait on the condition to
be notified and finally release the condition as follows:
...
# acquire the condition
await condition.acquire()
# wait to be notified
await condition.wait()
# release the condition
condition.release()
For example:
...
# acquire the condition
with condition:
# notify a waiting coroutines
condition.notify()
The notified coroutine will stop waiting as soon as it can reacquire the
condition. This will be attempted automatically as part of its call to wait(),
we do not need to do anything extra.
We can notify all coroutines waiting on the condition via the notify_all()
method.
...
# acquire the condition
with condition:
# notify all coroutines waiting on the condition
condition.notify_all()
Now that we know how to use condition variables, let’s look at a worked
example.
We will use a task to prepare some data and notify a waiting coroutine. In the
main coroutine, we will create and schedule the new task and use the
condition to wait for the work to be completed.
# task coroutine
async def task(condition, work_list):
# suspend for a moment
await asyncio.sleep(1)
# add data to the work list
work_list.append(33)
# notify a waiting coroutine that the work is done
print('Task sending notification...')
async with condition:
condition.notify()
# main coroutine
async def main():
# create a condition
condition = asyncio.Condition()
# prepare the work list
work_list = []
# wait to be notified that the data is ready
print('Main waiting for data...')
async with condition:
# create and start the task
_ = asyncio.create_task(
task(condition, work_list))
# wait to be notified
await condition.wait()
# we know the data is ready
print(f'Got data: {work_list}')
Running the example first creates the main() coroutine which is used as the
entry point into the asyncio program.
The main() coroutine runs and creates the shared condition and the work list.
The main() coroutine then acquires the condition. A new task is created and
scheduled, provided the shared condition and work list.
The main() coroutine then waits to be notified, suspending and calling the
new scheduled task to run.
The task() coroutine runs. It first suspends for a moment to simulate effort,
then adds work to the shared list. The condition is acquired and the waiting
coroutine is notified, then releases the condition automatically. The task
terminates.
The main() coroutine resumes and reports a final message, showing the
updated content of the shared list.
You now know how to use coroutine-safe queues to share data between
coroutines.
You now know how to use mutex locks to protect critical sections from
race conditions.
You now know how to use semaphores to limit concurrent access to a
resource for coroutines.
You now know how to use an event to signal between coroutines.
You now know how to coordinate coroutines with wait and notify using
a condition variable.
Exercise
Your task for this lesson is to use what you have learned about concurrency
primitives.
Develop a program where multiple tasks add and subtract from a single
shared integer value.
You could have one or more tasks that add one to a balance many times each
in a loop, and one or more tasks do the same by subtracting one from the
same shared global variable.
Have each task that modifies the global balance variable give many
opportunities for other coroutines to run, forcing a race condition.
For example:
# coroutine to add to the shared balance
async def add():
global balance
for i in range(10000):
tmp = balance
await asyncio.sleep(0)
tmp = tmp + 1
await asyncio.sleep(0)
balance = tmp
Confirm that the program results in a race condition by running the example
multiple times and getting different results.
Send me the link to your results, I’d love to see what you come up with.
Further Reading
This section provides resources for you to learn more about the topics
covered in this lesson.
Next
In the next lesson, we will explore how to run commands in subprocesses and
create, read from and write to streams asynchronously.
Lesson 06: Subprocesses and
Streams
In this lesson, we will explore how to run commands from an asyncio
program in subprocesses. We will also explore how we can implement socket
programming, such as opening a TCP socket connection then read and write
from it asynchronous using non-blocking I/O.
Commands can be called directly or executed via the user’s shell and will
execute in a subprocess.
And so on.
These are just programs that we can execute on the command line as a
command.
We may want to execute a command from our program for many reasons.
For example:
We may want to change the permissions of a file or change a system
configuration.
We may want to run a program to check the status of a resource or value
of a system property.
We may want to start another program in the background or for the user
to interact with.
The shell is a user interface for the command line, called a command line
interpreter.
For example, we can redirect the output of one command as input to another
command, such as the contents of the /etc/services file into the word count
wc command and count the number of lines:
cat /etc/services | wc -l
sh
bash
zsh
And so on.
The shell is already running, it was probably used to start the Python
program. We don’t need to do anything special to get or have access to the
shell.
Next, let’s look at how we can run commands from asyncio as subprocesses.
With asyncio.create_subprocess_exec()
With asyncio.create_subprocess_shell()
For example:
...
# run a command in a subprocess
process = await asyncio.create_subprocess_exec(
'echo', 'Hello World')
We can configure the subprocess to receive input from the asyncio program
or send output to the asyncio program by setting the stdin, stdout, and
stderr arguments to the asyncio.subprocess.PIPE constant.
This will set the stdin, stdout, and stderr arguments on the
asyncio.subprocess.Process to be a StreamReader or StreamWriter and
allow coroutines to read or write from them via the communicate() method in
the Process object, which we will explore further later.
For example:
...
# run a command in a subprocess
process = await asyncio.create_subprocess_exec(
'echo', 'Hello World',
stdout=asyncio.subprocess.PIPE)
The example below executes the echo command in a subprocess that prints
out the provided string.
The subprocess is started, then the details of the subprocess are then reported.
# main coroutine
async def main():
# run the command in a subprocess
process = await asyncio.create_subprocess_exec(
'echo', 'Hello World')
# report the details of the subprocess
print(f'subprocess: {process}')
# entry point
asyncio.run(main())
Running the example executes the echo command in a subprocess.
Executing a command via the shell allows the capabilities of the shell to be
used in addition to executing the command, such as wildcards and shell
variables.
The example below executes the echo command in a subprocess that prints
out the provided string. Unlike the create_subprocess_exec() function, the
entire command with arguments is provided as a single string.
The subprocess is started, then the details of the subprocess are then reported.
# main coroutine
async def main():
# run the command via shell in a subprocess
process = await asyncio.create_subprocess_shell(
'echo Hello World')
# report the details of the subprocess
print(f'subprocess: {process}')
# entry point
asyncio.run(main())
For example:
...
# run a command in a subprocess
process = await asyncio.create_subprocess_shell(
'sleep 3')
# wait for the subprocess to terminate
await process.wait()
Next, let’s look at how we can read and write data from a subprocess.
Reading data from the subprocess requires that the stdout or stderr
arguments of the create_subprocess_shell() or
create_subprocess_exec() functions was set to the PIPE constant.
No argument is provided and the method returns a tuple with input from
stdout and stderr. Data is read until an end-of-file (EOF) character is
received.
For example:
...
# run a command in a subprocess
process = await asyncio.create_subprocess_shell(
'echo Hello World', stdout=asyncio.subprocess.PIPE)
# read data from the subprocess
data, _ = await process.communicate()
If no data can be read, the call will suspend until the subprocess has
terminated.
We can write data to the subprocess from an asyncio coroutine also via the
communicate() method. Data is provided via the input argument as bytes.
Writing data to the subprocess via the communicate() method requires that
the stdin argument in the create_subprocess_shell() or
create_subprocess_exec() functions were set to the PIPE constant.
For example:
...
# run a command in a subprocess
process = await asyncio.create_subprocess_exec(
'cat', stdin=asyncio.subprocess.PIPE)
# write data to the subprocess
_ = await process.communicate(b'Hello World\n')
For example:
...
# terminate the subprocess
process.terminate()
On most platforms, this will send the SIGKILL signal to the subprocess in
order to stop it immediately.
Unlike the terminate() method that sends the SIGTERM signal, the SIGKILL
signal cannot be handled by the subprocess. This means it is assured to stop
the subprocess.
For example:
...
# kill the subprocess
process.kill()
Next, let’s move on from running commands from asyncio and explore how
we can open and use non-blocking I/O streams.
How to Use Non-Blocking I/O Streams
Asyncio provides non-blocking I/O socket programming.
Sockets can be opened that provide access to a stream writer and a stream
reader.
Data can then be written and read from the stream using coroutines,
suspending when appropriate.
The streams can also be used to create a server to handle requests using a
standard protocol, or to develop our own application-specific protocol.
Now that we know what asyncio streams are, let’s look at how to use them.
This is a coroutine that must be awaited and will return once the socket
connection is open.
For example:
...
# open a connection
reader, writer = await asyncio.open_connection(...)
The two required arguments are the host and the port.
The host is a string that specifies the server to connect to, such as a domain
name or an IP address.
The port is the socket port number, such as 80 for HTTP servers, 443 for
HTTPS servers, 23 for SMTP and so on.
For example:
...
# open a connection to an http server
reader, writer = await asyncio.open_connection(
'www.google.com', 80)
For example:
...
# open a connection to an https server
reader, writer = await asyncio.open_connection(
'www.google.com', 443, ssl=True)
For example:
...
# start a tcp server
server = await asyncio.start_server(...)
The three required arguments are the callback function, the host, and the port.
The host is the domain name or IP address that clients will specify to connect.
The port is the socket port number on which to receive connections, such as
21 for FTP or 80 for HTTP.
For example:
# handle connections
async def handler(reader, writer):
# ...
...
# start a server to receive http connections
server = await asyncio.start_server(
handler, '127.0.0.1', 80)
Byte data can be written to the socket using the write() method.
For example:
...
# write byte data
writer.write(byte_data)
Alternatively, multiple lines of byte data organized into a list or iterable can
be written using the writelines() method.
For example:
...
# write lines of byte data
writer.writelines(byte_lines)
After writing byte data it is a good idea to drain the socket via the drain()
method.
This is a coroutine and will suspend the caller until the bytes have been
transmitted and the socket is ready.
For example:
...
# write byte data
writer.write(byte_data)
# wait for data to be transmitted
await writer.drain()
String data can be converted into byte data for transmission by encoding it.
This can be achieved using the encode() method on the string which will
return byte data encoded with the default UTF8 encoding, ready for
transmission.
For example:
...
# encode string data to byte data for transmission
byte_data = string_data.encode()
Data is read in byte format, therefore strings may need to be encoded before
being used.
An arbitrary number of bytes can be read via the read() method, which will
read until the end of file (EOF).
...
# read byte data
byte_data = await reader.read()
Additionally, the number of bytes to read can be specified via the n argument.
This may be helpful if we know the number of bytes expected from the next
response.
For example:
...
# read byte data
byte_data = await reader.read(n=100)
This will return bytes until a new line character '\n' is encountered, or EOF.
This is helpful when reading standard protocols that operate with lines of
text.
...
# read a line data
byte_line = await reader.readline()
Data that is read from the stream can be decoded from bytes into string data
using the decode() method and the default UTF8 encoding.
For example:
...
# decode byte data into string data
string data = byte_data.decode()
Next, let’s look at how we can close an open TCP socket connection.
The close() method can be called which will close the socket. This method
does not suspend.
For example:
...
# close the socket
writer.close()
Although the close() method does not suspend, we can wait for the socket to
close completely before continuing on.
For example:
...
# close the socket
writer.close()
# wait for the socket to close
await writer.wait_closed()
We can check if the socket has been closed or is in the process of being
closed via the is_closing() method.
For example:
...
# check if the socket is closed or closing
if writer.is_closing():
# ...
Now that we know how to open and use asyncio streams, let’s look at a
worked example of checking the status of webpages asynchronously.
This can be achieved by issuing an HTTP GET request to each webpage and
reading the first line response which will contain the status of the webpage.
This requires first opening a socket connection to the HTTPS server on port
443 using SSL. We must then formulate the HTTP GET request that includes
the URL we desire and the host name. The string request must then be
encoded into byte data before being transmitted.
We can then read the first line of the response from the server, decode it and
return it as the HTTP status of the server. The TCP socket can then be closed.
This assumes the servers exist and that we can connect to it.
This process can be wrapped into a coroutine and executed concurrently for
each website URL that we wish to query.
# main coroutine
async def main():
# list of top 10 websites to check
sites = ['https://www.google.com/',
'https://www.youtube.com/',
'https://www.facebook.com/',
'https://twitter.com/',
'https://www.instagram.com/',
'https://www.baidu.com/',
'https://www.wikipedia.org/',
'https://yandex.ru/',
'https://yahoo.com/',
'https://www.whatsapp.com/']
# create all coroutine requests
coros = [get_status(url) for url in sites]
# execute all coroutines and wait
results = await asyncio.gather(*coros)
# process all results
for url, status in zip(sites, results):
# report status
print(f'{url:25}:\t{status}')
Running the example first creates the main() coroutine and uses it as the
entry point into the program.
The main() coroutine runs, defining a list of the top 10 websites to check.
The main() coroutine resumes and receives an iterable of status values. This
iterable along with the list of URLs is then traversed using the zip() built-in
function and the statuses are reported.
This highlights how we can open, write to, and read from multiple TCP
socket connections concurrently using non-blocking I/O.
https://www.google.com/ : HTTP/1.1 200 OK
https://www.youtube.com/ : HTTP/1.1 200 OK
https://www.facebook.com/: HTTP/1.1 302 Found
https://twitter.com/ : HTTP/1.1 200 OK
https://www.instagram.com/: HTTP/1.1 302 Found
https://www.baidu.com/ : HTTP/1.1 200 OK
https://www.wikipedia.org/: HTTP/1.1 200 OK
https://yandex.ru/ : HTTP/1.1 302 Moved ...
https://yahoo.com/ : HTTP/1.1 301 Moved ...
https://www.whatsapp.com/: HTTP/1.1 302 Found
Lesson Review
Takeaways
Exercise
Your task for this lesson is to expand upon the example that checks website
status.
Update the example to check the status of specific webpages you read often.
Further update the example to read the entire HTTP header for each URL and
report details from the header, at least the number of characters. This can be
achieved by reading lines until the first double new line is encountered.
Further update the example to read the HTTP body of the response and report
interesting details, such as the number of characters. The body begins right
after the header finishes with a double new line.
Send me the link to your results, I’d love to see what you come up with.
Further Reading
This section provides resources for you to learn more about the topics
covered in this lesson.
Next
In the next lesson, we will explore how to draw upon everything we have
learned and develop an asynchronous and concurrent port scanning program
using asyncio.
Lesson 07: Port Scanner Case Study
Asyncio coroutines can be used to scan multiple ports on a server
concurrently. This can dramatically speed up the process compared to
attempting to connect to each port, one by one. In this lesson, we will explore
how to develop a concurrent port scanner with asyncio.
How to open a socket connection to each port sequentially and how slow
it can be.
How to execute coroutines concurrently to scan ports and wait for them
to complete.
How to scan port numbers concurrently and report results dynamically
as soon as they are available.
Opening a socket requires both the name or IP address of the server and a
port number on which to connect.
For example, when our web browser opens a web page on python.org, it is
opening a socket connection to that server on port 80 or 443, then uses the
HTTP protocol to request and download (GET) an HTML file.
A simple way to implement a port scanner is to loop over all the ports we
want to test and attempt to make a socket connection on each. If a connection
can be made, we disconnect immediately and report that the port on the
server is open.
Next, let’s look at how we can open a socket connection on a single port.
How to Open a Socket Connection on a Port
We can open a
socket connection in asyncio using the
asyncio.open_connection() function.
This takes the host and port number and returns a StreamReader and
StreamWriter for interacting with the server via the socket.
For example:
...
# open a socket connection
reader, writer = asyncio.open_connection(
'python.org', 80)
If a port is not open, the call may wait for a long time before giving up. We
need a way to give up after a time limit.
This is a coroutine that will execute an awaitable and wait a fixed interval in
seconds before giving up and raising an asyncio.TimeoutError exception.
This will allow us to attempt to make a socket connection on a given port for
a fixed interval, such as one or three seconds.
For example:
...
# create coroutine for opening a connection
coro = asyncio.open_connection('python.org', 80)
# execute the coroutine with a timeout
try:
# open the connection and wait for a moment
_ = await asyncio.wait_for(coro, 1.0)
# ...
except asyncio.TimeoutError:
# ...
If the connection can be made within the time limit we can then close the
connection.
For example:
...
# close connection once opened
writer.close()
We can tie all of this together into a coroutine function that tests one port on
one host and returns True if the port is open or False otherwise.
Next, let’s look at how we can scan a large number of ports, one by one.
How to Scan a Range of Ports on a Server (slow)
We can scan a range of ports on a given host.
Many common internet services are provided on ports between 0 and 1,024.
The viable range of ports is 0 to 65,535, and we can see a list of the most
common port numbers and the services that use them in the file
/etc/services on POSIX systems.
The main() coroutine function below implements this reporting any open
ports that are discovered.
# main coroutine
async def main(host, ports):
# report a status message
print(f'Scanning {host}...')
# scan ports sequentially
for port in ports:
if await test_port_number(host, port):
print(f'> {host}:{port} [OPEN]')
Finally, we can call this function and specify the host and range of ports.
In this case, we will port scan python.org (out of love for Python, not
malicious intent).
...
# define a host and ports to scan
host = 'python.org'
ports = range(1, 1024)
# start the asyncio program
asyncio.run(main(host, ports))
We would expect that at least port 80 would be open for HTTP connections.
Tying this together, the complete example of port scanning a host with
asyncio is listed below.
# SuperFastPython.com
# example of an asyncio sequential port scanner
import asyncio
# main coroutine
async def main(host, ports):
# report a status message
print(f'Scanning {host}...')
# scan ports sequentially
for port in ports:
if await test_port_number(host, port):
print(f'> {host}:{port} [OPEN]')
Running the example attempts to make a connection for each port number
between 1 and 1,023 (one minus 1,024) and reports all open ports.
In this case, we can see that port 80 for HTTP is open as expected, and port
443 is also open for HTTPS.
Next, we will look at how to run coroutines concurrently to speed up this port
scanning process.
How to Scan Ports Concurrently (fast)
We can scan ports concurrently using asyncio.
This can be implemented by creating one coroutine for each port to scan, then
execute all coroutines concurrently and wait for them to complete. This can
be achieved using the asyncio.gather() function.
It requires first creating the coroutines. With one coroutine per port, this
would be a collection of more than 1,000 coroutines. We can achieve this
using a list comprehension.
For example:
...
# create all coroutines
coros = [test_port_number(host, port) for port in ports]
This function takes awaitables as arguments and will not return until the
awaitables are complete. It does not take a list of awaitables, therefore we
must expand our list into separate expressions using the star (*) operator.
For example:
...
# execute all coroutines concurrently
results = await asyncio.gather(*coros)
This will execute all coroutines concurrently and will return an iterable of
return values from each coroutine in the order provided.
We can then traverse the list of return values along with the list of ports and
report the results.
Recall that we can traverse two or more iterables together using the built-in
zip() function.
For example:
...
# report results
for port,result in zip(ports, results):
if result :
print(f'> {host}:{port} [OPEN]')
# main coroutine
async def main(host, ports):
# report a status message
print(f'Scanning {host}...')
# create all coroutines
coros = [test_port_number(host, port)
for port in ports]
# execute all coroutines concurrently
results = await asyncio.gather(*coros)
# report results
for port,result in zip(ports, results):
if result :
print(f'> {host}:{port} [OPEN]')
Running the example executes the main() coroutine as the entry point into
our asyncio program.
This suspends the main() coroutine until all coroutines are completed. Each
coroutine tests one port, attempting to open a connection and suspending it
until either the connection is open or the timeout is elapsed.
Once all tasks are completed the main() coroutine resumes and all results are
reported.
The big difference is the speed of execution. In this case, it takes about 3.1
seconds, compared to more than 50 minutes in the previous example.
That is about 3,063 seconds faster or a 989x speed-up, i.e. nearly 1000-times
faster.
Scanning python.org...
> python.org:80 [OPEN]
> python.org:443 [OPEN]
Next, let’s look at how we can report results as soon as they are available,
rather than after all coroutines complete.
How to Report Scan Results Dynamically
In the previous example, we executed the coroutines concurrently and
reported the results after all tasks had been completed.
This would allow the program to be more responsive and show results to the
user as they are available.
This function takes a collection of awaitables. If they are coroutines, they are
issued as tasks.
The function then returns an iterable of the coroutines that are yielded in the
order that they are completed.
We can traverse this iterable directly, we do not need to use the async for
expression reserved for asynchronous iterables.
For example:
...
# execute coroutines and handle results as they complete
for coro in asyncio.as_completed(coros):
# check the return value from the coroutine
# ...
The downside is that we don’t have an easy way to relate the coroutine to the
port that was tested. Therefore, we can update our test_port_number()
coroutine to return whether the port is open and the port number that was
tested.
For example:
# returns True if a connection can be made
async def test_port_number(host, port, timeout=3):
# create coroutine for opening a connection
coro = asyncio.open_connection(host, port)
# execute the coroutine with a timeout
try:
# open the connection and wait for a moment
_,writer = await asyncio.wait_for(coro, timeout)
# close connection once opened
writer.close()
# indicate the connection can be opened
return True,port
except asyncio.TimeoutError:
# indicate the connection cannot be opened
return False,port
We can then traverse the coroutines in the order they are completed and get
the details of the port and whether it is open from each and report it.
For example:
...
# execute coroutines and handle results as they complete
for coro in asyncio.as_completed(coros):
# check the return value from the coroutine
result, port = await coro
if result:
print(f'> {host}:{port} [OPEN]')
This will execute all coroutines concurrently and will report open ports as
they are discovered, rather than all at the end.
# main coroutine
async def main(host, ports):
# report a status message
print(f'Scanning {host}...')
# create all coroutines
coros = [test_port_number(host, port)
for port in ports]
# execute coroutines and handle results dynamically
for coro in asyncio.as_completed(coros):
# check the return value from the coroutine
result, port = await coro
if result:
print(f'> {host}:{port} [OPEN]')
Running the example executes the main() coroutine as the entry point into
the asyncio program.
This wraps each in another coroutine and executes them all concurrently and
independently.
The return value from each coroutine is retrieved and results are reported as
they are made available.
The example shows the same ports and executes in about the same time as
the previous concurrent examples, except the program is more responsive.
Ports are shown as open almost immediately, as opposed to after all ports in
the range have been checked and timed out.
Scanning python.org...
> python.org:80 [OPEN]
> python.org:443 [OPEN]
Lesson Review
Takeaways
Exercise
Your task for this lesson is to extend the above example for port scanning.
It is important that you know how to use the non-blocking I/O aspect of
asyncio in conjunction with the tools provided in the asyncio module. The
above example and these extensions will help you get comfortable with these
tools in this specific use case.
Send me the link to your results, I’d love to see what you come up with.
You can send me a message directly via:
Further Reading
This section provides resources for you to learn more about the topics
covered in this lesson.
Next
This was the last lesson, next we will take a look back at how far we have
come.
Conclusions
Look Back At How Far You’ve Come
Congratulations, you made it to the end of this 7-day course.
Let’s take a look back and review what you now know.
You discovered how to define, create, and run coroutines and how to use
the async/await expressions.
You discovered how to create asynchronous tasks, query their status,
cancel them and add callback functions.
You discovered how to run many coroutines concurrently in a group and
handle their results.
You discovered how to wait for many coroutines to complete, meet a
condition, or timeout.
You discovered how to define, create and use asynchronous iterators,
generators, and context managers.
You discovered how to use the async for and async with expressions
in asyncio programs.
You discovered how to synchronize and coordinate coroutines with
locks, semaphores, events and condition variables.
You discovered how to share data between coroutines using coroutine-
safe queues.
You discovered how to run, read, and write from subprocesses and
streams with coroutines.
You discovered how to develop a concurrent and dynamically updating
port scanner using non-blocking I/O.
You now know how to use the asyncio module and bring coroutine-based
concurrency to your project.
Thank you for letting me help you on your journey into Python concurrency.
APIs
Concurrent Execution API - Python Standard Library.
https://docs.python.org/3/library/concurrency.html
multiprocessing API - Process-based parallelism.
https://docs.python.org/3/library/multiprocessing.html
threading API - Thread-based parallelism.
https://docs.python.org/3/library/threading.html
concurrent.futures API - Launching parallel tasks.
https://docs.python.org/3/library/concurrent.futures.html
asyncio API - Asynchronous I/O.
https://docs.python.org/3/library/asyncio.html
Books
High Performance Python, Ian Ozsvald, et al., 2020.
https://amzn.to/3wRD5MX
Using AsyncIO in Python, Caleb Hattingh, 2020.
https://amzn.to/3lNp2ml
Python Concurrency with asyncio, Matt Fowler, 2022.
https://amzn.to/3LZvxNn
Effective Python, Brett Slatkin, 2019.
https://amzn.to/3GpopJ1
Python Cookbook, David Beazley, et al., 2013.
https://amzn.to/3MSFzBv
Python in a Nutshell, Alex Martelli, et al., 2017.
https://amzn.to/3m7SLGD
Getting More Help
Do you have any questions?
Below provides some great places online where you can ask questions about
Python programming and Python concurrency:
Stack Overview.
https://stackoverflow.com/
Python Subreddit.
https://www.reddit.com/r/python
LinkedIn Python Developers Community.
https://www.linkedin.com/groups/25827
Quora Python (programming language).
https://www.quora.com/topic/Python-programming-language-1
If you ever have any questions about the lessons in this book, please contact
me directly: