From 16d3b94e21986573ba01d07d36a4f04c8a96ee37 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 7 Oct 2018 20:18:13 +1000 Subject: [PATCH 01/31] Create basic structure of the asyncio tutorial --- .../asyncio-tutorial/async-functions.rst | 60 +++++++++ .../asyncio-tutorial/asyncio-cookbook.rst | 13 ++ .../asyncio-tutorial/case-study-cli.rst | 12 ++ .../asyncio-tutorial/case-study-gui.rst | 20 +++ Doc/library/asyncio-tutorial/index.rst | 24 ++++ .../running-async-functions.rst | 11 ++ Doc/library/asyncio-tutorial/what-asyncio.rst | 123 ++++++++++++++++++ Doc/library/asyncio-tutorial/why-asyncio.rst | 35 +++++ Doc/library/asyncio.rst | 1 + 9 files changed, 299 insertions(+) create mode 100644 Doc/library/asyncio-tutorial/async-functions.rst create mode 100644 Doc/library/asyncio-tutorial/asyncio-cookbook.rst create mode 100644 Doc/library/asyncio-tutorial/case-study-cli.rst create mode 100644 Doc/library/asyncio-tutorial/case-study-gui.rst create mode 100644 Doc/library/asyncio-tutorial/index.rst create mode 100644 Doc/library/asyncio-tutorial/running-async-functions.rst create mode 100644 Doc/library/asyncio-tutorial/what-asyncio.rst create mode 100644 Doc/library/asyncio-tutorial/why-asyncio.rst diff --git a/Doc/library/asyncio-tutorial/async-functions.rst b/Doc/library/asyncio-tutorial/async-functions.rst new file mode 100644 index 00000000000000..3a1101f049f6cb --- /dev/null +++ b/Doc/library/asyncio-tutorial/async-functions.rst @@ -0,0 +1,60 @@ +Functions: Sync vs Async +======================== + +Regular Python functions are created with the keyword ``def``, +and look like this: + +.. code-block:: python + + def f(x, y): + print(x + y) + + f(1, 2) + +The snippet also shows how the function is evaluated. Async functions are +different in two respects: + +.. code-block:: python + + import asyncio + + async def f(x, y): + print(x + y) + + asyncio.run(f(1, 2)) + +The first difference is that the function declaration is spelled +``async def``. The second difference is that async functions cannot be +executed by simply evaluating them. Here, we use the ``run()`` function +from the ``asyncio`` module. + +The ``run`` function is only good for executing an async function +from "synchronous" code; and this is usually only used to execute +a "main" async function, from which others can be called in a simpler +way. + +That means the following: + +.. code-block:: python + + import asyncio + + async def f(x, y): + print(x + y) + + async def main(): + await f(1, 2) + + asyncio.run(main()) + + +The execution of the first async function, ``main()``, is performed +with ``run()``, but once you're inside an ``async def`` function, then +all you need to execute another async function is the ``await`` keyword. + +TODO: +- which kind of functions can be called from which other kind +- use the "inspect" module to verify the formal names of functions, +coroutine functions, coroutines, etc. + + diff --git a/Doc/library/asyncio-tutorial/asyncio-cookbook.rst b/Doc/library/asyncio-tutorial/asyncio-cookbook.rst new file mode 100644 index 00000000000000..fd4979ceedd37d --- /dev/null +++ b/Doc/library/asyncio-tutorial/asyncio-cookbook.rst @@ -0,0 +1,13 @@ +Asyncio Cookbook +================ + +Let's look at a few common situations that will come up in your +``asyncio`` programs, and how best to tackle them. + +TODO + +Notes: + +- My thinking here was a Q&A style, and then each section has + a code snippet demonstrating the answer. + diff --git a/Doc/library/asyncio-tutorial/case-study-cli.rst b/Doc/library/asyncio-tutorial/case-study-cli.rst new file mode 100644 index 00000000000000..482bd24b3500d4 --- /dev/null +++ b/Doc/library/asyncio-tutorial/case-study-cli.rst @@ -0,0 +1,12 @@ +Asyncio Case Study: Chat Application +==================================== + +TODO + +Notes: + +- using the streams API +- first show the client. +- will have to explain a message protocol. (But that's easy) +- then show the server +- spend some time on clean shutdown. diff --git a/Doc/library/asyncio-tutorial/case-study-gui.rst b/Doc/library/asyncio-tutorial/case-study-gui.rst new file mode 100644 index 00000000000000..0f693d7613bcc9 --- /dev/null +++ b/Doc/library/asyncio-tutorial/case-study-gui.rst @@ -0,0 +1,20 @@ +Asyncio Case Study: Chat Application with GUI client +==================================================== + +TODO + +Notes: + +- server code remains identical to prior case study +- focus is on making a nice client +- The focus area here is: can you use asyncio if there is another + blocking "loop" in the main thread? (common with GUIs and games) + How do you do that? +- Mention any special considerations +- Show and discuss strategies for passing data between main thread + (GUI) and the asyncio thread (IO). +- We can demonstrate the above with tkinter, allowing the + case study to depend only on the stdlib +- Towards the end, mention how the design might change if + the client was a browser instead of a desktop client. + (can refer to the 3rd party websocket library, or aiohttp) diff --git a/Doc/library/asyncio-tutorial/index.rst b/Doc/library/asyncio-tutorial/index.rst new file mode 100644 index 00000000000000..0d7cf0613da782 --- /dev/null +++ b/Doc/library/asyncio-tutorial/index.rst @@ -0,0 +1,24 @@ +Asyncio Tutorial +================ + +Programming with ``async def`` functions is different to normal Python +functions; enough so that it is useful to explain a bit more +about what ``asyncio`` is for, and how to use it in typical +programs. + +This tutorial will focus on what an end-user of ``asyncio`` should +learn to get the most value out of it. Our focus is going to be +primarily on the "high-level" API, as described in the +:mod:`documentation `. + + +.. toctree:: + :maxdepth: 1 + + what-asyncio.rst + why-asyncio.rst + async-functions.rst + running-async-functions.rst + asyncio-cookbook.rst + case-study-cli.rst + case-study-gui.rst diff --git a/Doc/library/asyncio-tutorial/running-async-functions.rst b/Doc/library/asyncio-tutorial/running-async-functions.rst new file mode 100644 index 00000000000000..d55386c571b2eb --- /dev/null +++ b/Doc/library/asyncio-tutorial/running-async-functions.rst @@ -0,0 +1,11 @@ +Executing async functions +========================= + +TODO + +Notes: + +- can be called by other async functions +- can NOT be called by sync functions +- can be executed by ``asyncio.run()`` +- can be executed in task by ``asyncio.create_task()`` diff --git a/Doc/library/asyncio-tutorial/what-asyncio.rst b/Doc/library/asyncio-tutorial/what-asyncio.rst new file mode 100644 index 00000000000000..0f4a7f4ed46352 --- /dev/null +++ b/Doc/library/asyncio-tutorial/what-asyncio.rst @@ -0,0 +1,123 @@ +What Does Async Mean? +===================== + +Let's make a function that communicates over the network: + +.. code-block:: python + + import socket + + def greet(host, port): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((host, host)) + s.sendall(b'Hello, world') + reply = s.recv(1024) + + print('Reply:', repr(reply)) + +This function will: + +#. make a socket connection to a host, +#. send a greeting, and +#. wait for a reply. + +The key point here is about the word *wait*: execution proceeds line-by-line +until the line ``reply = s.recv(1024)``. We call this behaviour +*synchronous*. At this point, execution pauses +until we get a reply from the host. This is as we expect: +Python only ever executes one line at a time. + +Now the question comes up: what if you need to send a greeting to +*multiple* hosts? You could just call it twice, right? + +.. code-block:: python + + import socket + + def greet(host, port): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((host, host)) + s.sendall(b'Hello, world') + reply = s.recv(1024) + + print('Reply:', repr(reply)) + + greet(host1, port1) + greet(host2, port2) + +This works fine, but see that the first call to ``greet`` will completely +finish before the second call (to ``host2``) can begin. + +Computers process things sequentially, which is why programming languages +like Python also work sequentially; but what we see here is that our +code is also going to **wait** sequentially. This is, quite literally, +a waste of time. What we would really like to do here is wait for the +all the replies *concurrently*, i.e., at the same time. + +Operating systems like Windows, Mac and Linux, and others, understand +this problem deeply. If you're reading this on a computer or even your +mobile, there will be tens or hundreds of processes running at the same +time on your device. At least, they will appear to be running +concurrently. What is really happening is that the operating system +is sharing little slices of processor (CPU) time among all the +processes. If we started two copies of our ``greet`` program at the +same time, then they *would* run (and therefore wait) concurrently. + +However, there is a price for that: each new process consumes resources +from the operating system. But more than that, there is another tricky +problem about *how* the operating system knows when to allocate +execution time between each process. The answer: it doesn't! This means +that the operating system can decide when to give processor time to each +process. + +And this means that you will never be sure of when each of your processes +is actually running, relative to each other. This is quite safe because +processes are isolated from each other; however, **threads** are not +isolated from each other. In fact, the primary feature of threads over +processes is that multiple threads within a single process can +access the same memory. And this is where all the problems appear. + +So: we can also run the ``greet()`` function in multiple threads, and then +they will also wait for replies concurrently. However, now you have +two threads that is allowed to access the same objects, with no control over +how execution will be transferred between the two threads. This +situation can result in *race conditions* in how objects are modified, +and these bugs can be very difficult to debug. + +This is where "async" programming comes in. It provides a way to manage +multiple socket connections all in a single thread; and the best part +is that you get to control *when* execution is allowed to switch between +these different contexts. + +We will explain more of the details later on in the tutorial, +but briefly, our earlier example becomes something like the following +pseudocode: + +.. code-block:: python + + import asyncio + + async def greet(host, port): + reader, writer = await asyncio.open_connection(host, port) + writer.write(b'Hello, world') + reply = await reader.recv(1024) + writer.close() + + print('Reply:', repr(reply)) + + async def main(): + await asyncio.gather( + greet(host1, port1), + greet(host2, port2) + ) + + asyncio.run(main()) + +There are a couple of new things here, but I want you to focus +on the new keyword ``await``. Unlike threads, execution is allowed to +switch between the two ``greet()`` invocations **only** where the +``await`` keyword appears. On all other lines, execution is exactly the +same as normal Python. These ``async def`` functions are called +"asynchronous" because execution does not pass through the function +top-down, but instead can suspend in the middle of a function at the +``await`` keyword, and allow another function to execute. diff --git a/Doc/library/asyncio-tutorial/why-asyncio.rst b/Doc/library/asyncio-tutorial/why-asyncio.rst new file mode 100644 index 00000000000000..5a88bccc8afe07 --- /dev/null +++ b/Doc/library/asyncio-tutorial/why-asyncio.rst @@ -0,0 +1,35 @@ +Why Asyncio? +============ + +There are two very specific reasons for using ``async def`` functions: + +#. Safety: easier reasoning about concurrency, and virtually + eliminate `memory races `_ + in concurrent network code +#. High concurrency: huge number of open socket connections + +Safety +------ + +- async/await makes all context switches visible; that makes it easy + to spot race conditions and + `reason about your code `_ + +- in general, all datastructures are safe for async (we cannot say same + for threads) + +- an async/await library means that it's safe to use it in concurrent + async/await code (you can never be sure if some library is thread-safe, + even if it claims that) + +- language constructs like 'async for' and 'async with' enable structured + concurrency + +High Concurrency +---------------- + +- high-throughput IO or 1000s of long-living connections are only + doable with asyncio + +- if you don't need to scale your code right now but might need + in near future investing in async/await is wise diff --git a/Doc/library/asyncio.rst b/Doc/library/asyncio.rst index 6990adb21e3603..233a7f05520ab9 100644 --- a/Doc/library/asyncio.rst +++ b/Doc/library/asyncio.rst @@ -88,6 +88,7 @@ Additionally, there are **low-level** APIs for :caption: Guides and Tutorials :maxdepth: 1 + asyncio-tutorial/index.rst asyncio-api-index.rst asyncio-llapi-index.rst asyncio-dev.rst From 50a901eba6b359d0181c1cf31663448cb9769d66 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 14 Oct 2018 14:16:03 +1000 Subject: [PATCH 02/31] Begun work on the case study for the server --- ...cli.rst => case-study-chat-client-cli.rst} | 0 ...gui.rst => case-study-chat-client-gui.rst} | 0 .../case-study-chat-server.rst | 154 ++++++++++++++++++ Doc/library/asyncio-tutorial/index.rst | 5 +- .../running-async-functions.rst | 2 +- Doc/library/asyncio-tutorial/server01.py | 7 + Doc/library/asyncio-tutorial/server02.py | 17 ++ Doc/library/asyncio-tutorial/server20.py | 73 +++++++++ Doc/library/asyncio-tutorial/utils20.py | 59 +++++++ Doc/library/asyncio-tutorial/what-asyncio.rst | 35 ++-- 10 files changed, 339 insertions(+), 13 deletions(-) rename Doc/library/asyncio-tutorial/{case-study-cli.rst => case-study-chat-client-cli.rst} (100%) rename Doc/library/asyncio-tutorial/{case-study-gui.rst => case-study-chat-client-gui.rst} (100%) create mode 100644 Doc/library/asyncio-tutorial/case-study-chat-server.rst create mode 100644 Doc/library/asyncio-tutorial/server01.py create mode 100644 Doc/library/asyncio-tutorial/server02.py create mode 100644 Doc/library/asyncio-tutorial/server20.py create mode 100644 Doc/library/asyncio-tutorial/utils20.py diff --git a/Doc/library/asyncio-tutorial/case-study-cli.rst b/Doc/library/asyncio-tutorial/case-study-chat-client-cli.rst similarity index 100% rename from Doc/library/asyncio-tutorial/case-study-cli.rst rename to Doc/library/asyncio-tutorial/case-study-chat-client-cli.rst diff --git a/Doc/library/asyncio-tutorial/case-study-gui.rst b/Doc/library/asyncio-tutorial/case-study-chat-client-gui.rst similarity index 100% rename from Doc/library/asyncio-tutorial/case-study-gui.rst rename to Doc/library/asyncio-tutorial/case-study-chat-client-gui.rst diff --git a/Doc/library/asyncio-tutorial/case-study-chat-server.rst b/Doc/library/asyncio-tutorial/case-study-chat-server.rst new file mode 100644 index 00000000000000..7bfceec46d018b --- /dev/null +++ b/Doc/library/asyncio-tutorial/case-study-chat-server.rst @@ -0,0 +1,154 @@ +Asyncio Case Study: Chat Application (Server) +============================================= + +We're going to build a chat application. Users will be able to +connect to a central server and send messages to "chat rooms", which +all the other users in those rooms will be able to see. This case study +gives us an opportunity to show how to use the various features of +``asyncio`` in a "real world" application. + +This will be a client-server application. The server application +will run on a central host, and the chat application will run on each +user's computer or device. The server is the easier of the two, for +interesting reasons that we'll get into later. + +We're going to start with a basic application layout and build up from +there. + +Starting Code Layout +-------------------- + +The code below shows the basic starting template for an asyncio +service. In these kinds of applications, the service will typically +run for a long time, serving clients that may connect, +disconnect, and then later reconnect multiple times. + +.. literalinclude:: server01.py + :language: python + +As explained earlier, ``main`` itself is a *coroutine function*, and +when evaluated, i.e., ``main()``, it returns a *coroutine* object +which the ``asyncio.run()`` function knows how to execute. + +.. note:: + The ``asyncio.run()`` function waits for ``main()`` to complete. + When ``main()`` returns, the ``run()`` function will then cancel + all tasks that are still around. This means, precisely, that the + ``asyncio.CancelledError`` exception will get raised in all such + pending tasks. This gives you a way to deal with an "application + shutdown" scenario: you just need to handle the ``CancelledError`` + exception in places where you need a controlled way of terminating + tasks. + +There isn't much more to say about the basic template, so let's add +the actual server. + +Server +------ + +We can use the *Streams API* (ref:TODO) to create a TCP server very +easily: + +.. literalinclude:: server02.py + :language: python + :linenos: + +We've added the ``start_server()`` call on line 5, and this call takes +not only the ``host`` and ``port`` parameters you'd expect, but also a +*callback* function that will be called for each new connection. This +is coroutine function ``client_connected()``, on line 13. + +The callback is provided with ``reader`` and ``writer`` parameters. +These give access to two streams for this new connection. It is here +that data will be received from, and sent to clients. + +Printing out "New client connected!" is obviously going to be quite +useless. We're going to want to receive chat messages from a client, +and we also want to send these messages to all the other clients in the +same "chat room". We don't yet have the concept of "rooms" defined +anywhere yet, but that's ok. Let's first focus on what must be sent +and received between server and client. + +Let's sketch out a basic design of the communication pattern: + +#. Client connects to server +#. Client sends a message to server to announce themselves, and join + a room +#. Client sends a message to a room +#. Server relays that message to all other clients in the same room +#. Eventually, client disconnects. + +These actions suggest a few different kinds of information that need to +be sent between server and client. We need to create a *protocol* +that both server and client can use to communicate. + +How about we use JSON messages? Here is an example of the payload a +client needs to provide immediately after connection: + +.. code-block:: json + :caption: Client payload after connection + + { + "action": "connect", + "username": "" + } + +Here are example messages for joining and leaving rooms: + +.. code-block:: json + :caption: Client payload to join a room + + { + "action": "joinroom", + "room": "" + } + +.. code-block:: json + :caption: Client payload to leave a room + + { + "action": "leaveroom", + "room": "" + } + +And here's an example of a client payload for sending a chat +message to a room: + +.. code-block:: json + :caption: Client payload to send a message to a room + + { + "action": "chat", + "room": "", + "message": "I'm reading the asyncio tutorial!" + } + +All of the JSON examples above are for payloads that will be received +from clients, but remember that the server must also send messages +to all clients in a room. That message might look something like this: + +.. code-block:: json + :caption: Server payload to update all clients in a room + :linenos: + + { + "action": "chat", + "room": "", + "message": "I'm reading the asyncio tutorial!", + "from": "" + } + +The message is similar to the one received by a client, but on line 5 +we now need to indicate from whom the message was sent. + + + +TODO + +Notes: + +- using the streams API +- first show the client. +- will have to explain a message protocol. (But that's easy) +- then show the server +- spend some time on clean shutdown. diff --git a/Doc/library/asyncio-tutorial/index.rst b/Doc/library/asyncio-tutorial/index.rst index 0d7cf0613da782..30671bb5a449da 100644 --- a/Doc/library/asyncio-tutorial/index.rst +++ b/Doc/library/asyncio-tutorial/index.rst @@ -20,5 +20,6 @@ primarily on the "high-level" API, as described in the async-functions.rst running-async-functions.rst asyncio-cookbook.rst - case-study-cli.rst - case-study-gui.rst + case-study-chat-server.rst + case-study-chat-client-cli.rst + case-study-chat-client-gui.rst diff --git a/Doc/library/asyncio-tutorial/running-async-functions.rst b/Doc/library/asyncio-tutorial/running-async-functions.rst index d55386c571b2eb..bca19d78e73bc7 100644 --- a/Doc/library/asyncio-tutorial/running-async-functions.rst +++ b/Doc/library/asyncio-tutorial/running-async-functions.rst @@ -1,4 +1,4 @@ -Executing async functions +Executing Async Functions ========================= TODO diff --git a/Doc/library/asyncio-tutorial/server01.py b/Doc/library/asyncio-tutorial/server01.py new file mode 100644 index 00000000000000..7c938e422a283d --- /dev/null +++ b/Doc/library/asyncio-tutorial/server01.py @@ -0,0 +1,7 @@ +import asyncio + +async def main(): + ... + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/Doc/library/asyncio-tutorial/server02.py b/Doc/library/asyncio-tutorial/server02.py new file mode 100644 index 00000000000000..554721ab3ca8eb --- /dev/null +++ b/Doc/library/asyncio-tutorial/server02.py @@ -0,0 +1,17 @@ +import asyncio +from asyncio import StreamReader, StreamWriter + +async def main(): + server = await asyncio.start_server( + client_connected_cb=client_connected, + host='localhost', + port='9011', + ) + async with server: + await server.serve_forever() + +async def client_connected(reader: StreamReader, writer: StreamWriter): + print('New client connected!') + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/Doc/library/asyncio-tutorial/server20.py b/Doc/library/asyncio-tutorial/server20.py new file mode 100644 index 00000000000000..65d4dd457c83b5 --- /dev/null +++ b/Doc/library/asyncio-tutorial/server20.py @@ -0,0 +1,73 @@ +import asyncio +from collections import defaultdict +from weakref import WeakValueDictionary +import json +import utils +import ssl + + +WRITERS = WeakValueDictionary() +ROOMS = defaultdict(WeakValueDictionary) + + +async def sender(addr, writer, room, msg): + try: + await utils.send_message( + writer, + json.dumps(dict(room=room, msg=msg)).encode() + ) + except (ConnectionAbortedError, ConnectionResetError): + """ Connection is dead, remove it.""" + if addr in WRITERS: + del WRITERS[addr] + if addr in ROOMS[room]: + del ROOMS[room][addr] + + +def send_to_room(from_addr, room: str, msg: str): + """Send the message to all clients in the room.""" + for addr, writer in ROOMS[room].items(): + print(f'Sending message to {addr} in room {room}: {msg}') + asyncio.create_task(sender(addr, writer, room, msg)) + + +async def client_connected_cb(reader, writer): + addr = writer.get_extra_info('peername') + print(f'New connection from {addr}') + WRITERS[addr] = writer + async for msg in utils.messages(reader): + print(f'Received bytes: {msg}') + d = json.loads(msg) + if d.get('action') == 'join': + ROOMS[d['room']][addr] = writer + elif d.get('action') == 'leave': + del ROOMS[d['room']][addr] + else: + d['from'] = addr + send_to_room(addr, d['room'], d['msg']) + + +async def main(): + ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ctx.check_hostname = False + ctx.load_cert_chain('chat.crt', 'chat.key') + server = await asyncio.start_server( + client_connected_cb=client_connected_cb, + host='localhost', + port='9011', + ssl=ctx, + ) + shutdown = asyncio.Future() + utils.install_signal_handling(shutdown) + print('listening...') + async with server: + done, pending = await asyncio.wait( + [server.serve_forever(), shutdown], + return_when=asyncio.FIRST_COMPLETED + ) + if shutdown.done(): + return + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/Doc/library/asyncio-tutorial/utils20.py b/Doc/library/asyncio-tutorial/utils20.py new file mode 100644 index 00000000000000..883d14a2681ef3 --- /dev/null +++ b/Doc/library/asyncio-tutorial/utils20.py @@ -0,0 +1,59 @@ +import sys +from asyncio import (StreamReader, StreamWriter, IncompleteReadError, Future, + get_running_loop) + +if sys.platform == 'win32': + from signal import signal, SIGBREAK, SIGTERM, SIGINT +else: + SIGBREAK = None + from signal import signal, SIGTERM, SIGINT + +from typing import AsyncGenerator + + +async def messages(reader: StreamReader) -> AsyncGenerator[bytes, None]: + """Async generator to return messages as they come in.""" + try: + while True: + size_prefix = await reader.readexactly(4) + size = int.from_bytes(size_prefix, byteorder='little') + message = await reader.readexactly(size) + yield message + except (IncompleteReadError, ConnectionAbortedError, ConnectionResetError): + return + + +async def send_message(writer: StreamWriter, message: bytes): + """To close the connection, use an empty message.""" + if not message: + writer.close() + await writer.wait_closed() + return + size_prefix = len(message).to_bytes(4, byteorder='little') + writer.write(size_prefix) + writer.write(message) + await writer.drain() + + +def install_signal_handling(fut: Future): + """Given future will be set a signal is received. This + can be used to control the shutdown sequence.""" + if sys.platform == 'win32': + sigs = SIGBREAK, SIGINT + loop = get_running_loop() + + def busyloop(): + """Required to handle CTRL-C quickly on Windows + https://bugs.python.org/issue23057 """ + loop.call_later(0.1, busyloop) + + loop.call_later(0.1, busyloop) + else: + sigs = SIGTERM, SIGINT + + # Signal handlers. Windows is a bit tricky + for s in sigs: + signal( + s, + lambda *args: loop.call_soon_threadsafe(fut.set_result, None) + ) diff --git a/Doc/library/asyncio-tutorial/what-asyncio.rst b/Doc/library/asyncio-tutorial/what-asyncio.rst index 0f4a7f4ed46352..c6ac436d3f7688 100644 --- a/Doc/library/asyncio-tutorial/what-asyncio.rst +++ b/Doc/library/asyncio-tutorial/what-asyncio.rst @@ -1,5 +1,5 @@ -What Does Async Mean? -===================== +What Does "Async" Mean? +======================= Let's make a function that communicates over the network: @@ -54,6 +54,9 @@ code is also going to **wait** sequentially. This is, quite literally, a waste of time. What we would really like to do here is wait for the all the replies *concurrently*, i.e., at the same time. +Preemptive Concurrency +---------------------- + Operating systems like Windows, Mac and Linux, and others, understand this problem deeply. If you're reading this on a computer or even your mobile, there will be tens or hundreds of processes running at the same @@ -68,22 +71,34 @@ from the operating system. But more than that, there is another tricky problem about *how* the operating system knows when to allocate execution time between each process. The answer: it doesn't! This means that the operating system can decide when to give processor time to each -process. - -And this means that you will never be sure of when each of your processes -is actually running, relative to each other. This is quite safe because +process. Your code, and therefore you, will not know when these switches +occur. This is called "preemption". From +`Wikipedia `_: +*In computing, preemption is the act of temporarily interrupting a +task being carried out by a computer system, without requiring +its cooperation, and with the intention of resuming the task +at a later time*. + +This means that you will never be sure of when each of your processes +is *actually* executing on a CPU. This is quite safe because processes are isolated from each other; however, **threads** are not isolated from each other. In fact, the primary feature of threads over processes is that multiple threads within a single process can -access the same memory. And this is where all the problems appear. +access the same memory. And this is where all the problems begin. -So: we can also run the ``greet()`` function in multiple threads, and then +Jumping back to our code sample further up: we may also choose to run the +``greet()`` function in multiple threads; and then they will also wait for replies concurrently. However, now you have -two threads that is allowed to access the same objects, with no control over -how execution will be transferred between the two threads. This +two threads that are allowed to access the same objects in memory, +with no control over +how execution will be transferred between the two threads (unless you +use the synchronization primitives in the ``threading`` module) . This situation can result in *race conditions* in how objects are modified, and these bugs can be very difficult to debug. +Cooperative Concurrency +----------------------- + This is where "async" programming comes in. It provides a way to manage multiple socket connections all in a single thread; and the best part is that you get to control *when* execution is allowed to switch between From dfede40791ad404ae7e7178771bdc515bfe334d1 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 21 Oct 2018 14:15:37 +1000 Subject: [PATCH 03/31] Incorporate review comments from @willingc --- Doc/library/asyncio-tutorial/async-functions.rst | 10 +++++++--- Doc/library/asyncio-tutorial/index.rst | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Doc/library/asyncio-tutorial/async-functions.rst b/Doc/library/asyncio-tutorial/async-functions.rst index 3a1101f049f6cb..89d0f57040ab06 100644 --- a/Doc/library/asyncio-tutorial/async-functions.rst +++ b/Doc/library/asyncio-tutorial/async-functions.rst @@ -9,10 +9,10 @@ and look like this: def f(x, y): print(x + y) + # Evaluate the function f(1, 2) -The snippet also shows how the function is evaluated. Async functions are -different in two respects: +Async functions are different in two respects: .. code-block:: python @@ -21,13 +21,17 @@ different in two respects: async def f(x, y): print(x + y) + # Execute the *async* function above asyncio.run(f(1, 2)) The first difference is that the function declaration is spelled ``async def``. The second difference is that async functions cannot be -executed by simply evaluating them. Here, we use the ``run()`` function +executed by simply evaluating them. Instead, we use the ``run()`` function from the ``asyncio`` module. +Executing Async Functions +------------------------- + The ``run`` function is only good for executing an async function from "synchronous" code; and this is usually only used to execute a "main" async function, from which others can be called in a simpler diff --git a/Doc/library/asyncio-tutorial/index.rst b/Doc/library/asyncio-tutorial/index.rst index 30671bb5a449da..ee4d958aa91ffd 100644 --- a/Doc/library/asyncio-tutorial/index.rst +++ b/Doc/library/asyncio-tutorial/index.rst @@ -1,7 +1,7 @@ Asyncio Tutorial ================ -Programming with ``async def`` functions is different to normal Python +Programming with ``async def`` functions is differs from normal Python functions; enough so that it is useful to explain a bit more about what ``asyncio`` is for, and how to use it in typical programs. From a11e659339a2f9551db039a3dcb220a18f8cf935 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 21 Oct 2018 14:28:11 +1000 Subject: [PATCH 04/31] Refine language around threads and processes --- Doc/library/asyncio-tutorial/what-asyncio.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Doc/library/asyncio-tutorial/what-asyncio.rst b/Doc/library/asyncio-tutorial/what-asyncio.rst index c6ac436d3f7688..f0b58cce3148f0 100644 --- a/Doc/library/asyncio-tutorial/what-asyncio.rst +++ b/Doc/library/asyncio-tutorial/what-asyncio.rst @@ -80,11 +80,13 @@ its cooperation, and with the intention of resuming the task at a later time*. This means that you will never be sure of when each of your processes -is *actually* executing on a CPU. This is quite safe because -processes are isolated from each other; however, **threads** are not -isolated from each other. In fact, the primary feature of threads over -processes is that multiple threads within a single process can -access the same memory. And this is where all the problems begin. +and threads is *actually* executing on a CPU. For processes, this +is quite safe because +their memory spaces are isolated from each other; however, +**threads** are not isolated from each other. In fact, the primary +feature of threads over processes is that multiple threads within a +single process can access the same memory. And this is where all the +problems begin. Jumping back to our code sample further up: we may also choose to run the ``greet()`` function in multiple threads; and then From 7e205d28117e67ec7472fad6e303b83b87e07635 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 21 Oct 2018 16:46:14 +1000 Subject: [PATCH 05/31] Incorporate message handling into server code --- .../case-study-chat-server.rst | 153 +++++++++++++++++- Doc/library/asyncio-tutorial/server03.py | 41 +++++ Doc/library/asyncio-tutorial/utils01.py | 29 ++++ 3 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 Doc/library/asyncio-tutorial/server03.py create mode 100644 Doc/library/asyncio-tutorial/utils01.py diff --git a/Doc/library/asyncio-tutorial/case-study-chat-server.rst b/Doc/library/asyncio-tutorial/case-study-chat-server.rst index 7bfceec46d018b..4a4ec06e0c50fe 100644 --- a/Doc/library/asyncio-tutorial/case-study-chat-server.rst +++ b/Doc/library/asyncio-tutorial/case-study-chat-server.rst @@ -141,14 +141,161 @@ to all clients in a room. That message might look something like this: The message is similar to the one received by a client, but on line 5 we now need to indicate from whom the message was sent. +Message-based Protocol +---------------------- +Now we have a rough idea about what messages between client and +server will look like. Unfortunately, the Streams API, like the +underlying TCP protocol it wraps, does not give us message-handling +built-in. All we get is a stream of bytes. It is up to us to +know how to break up the stream of bytes into recognizable messages. + +The most common raw message structure is based on the idea of a +*size prefix*: + +.. code-block:: text + :caption: Simple message structure, in bytes + + [3 bytes (header)][n bytes (payload)] + +The 3-byte header is an encoded 24-bit integer, which must be the size +of the following payload (in bytes). Using 3 bytes is arbitrary: I +chose it here because it allows a message size of up to 16 MB, which +is way, way larger than what we're going to need for this tutorial. + +Receiving A Message +^^^^^^^^^^^^^^^^^^^ + +Imagine we receive a message over a ``StreamReader`` instance. +Remember, this is what we get from the ``client_connected()`` +callback function. This is how we pull the bytes off the stream +to reconstruct the message: + +.. code-block:: python3 + :linenos: + + from asyncio import StreamReader + from typing import AsyncGenerator + import json + + async def new_messages(reader: StreamReader) -> AsyncGenerator[Dict, None]: + while True: + size_prefix = await reader.readexactly(3) + size = int.from_bytes(size_prefix, byteorder='little') + message = await reader.readexactly(size) + yield json.loads(message) + +This code shows the following: + +- line 5: This is an async function, but it's also a *generator*. All + that means is that there is a ``yield`` inside, that will produce + each new message as it comes in. +- line 6: A typical "infinite loop"; this async function will keep + running, and keep providing newly received messages back to the + caller code. +- line 7: We read exactly 3 bytes off the stream. +- line 8: Convert those 3 bytes into an integer object. Note that the + byteorder here, "little", must also be used in client code that + will be connecting to this server. +- line 9: Using the size calculated above, read exactly that number of + bytes off the stream +- line 10: decode the bytes from JSON and yield that dict out. + +The code sample above is pretty simple, but a little naive. There +are several ways in which it can fail and end the ``while True`` +loop. Let's add a bit more error handling: + +.. code-block:: python3 + :caption: Receiving JSON Messages over a Stream + :linenos: + + from asyncio import StreamReader, IncompleteReadError, CancelledError + from typing import AsyncGenerator + import json + + async def new_messages(reader: StreamReader) -> AsyncGenerator[Dict, None]: + try: + while True: + size_prefix = await reader.readexactly(3) + size = int.from_bytes(size_prefix, byteorder='little') + message = await reader.readexactly(size) + try: + yield json.loads(message) + except json.decoder.JSONDecodeError: + continue + except (IncompleteReadError, ConnectionAbortedError, + ConnectionResetError, CancelledError): + # The connection is dead, leave. + return + +Now, we fail completely on a connection error, but if a particular +payload fails to deserialize via JSON, then we handle that too, but +allow the loop to continue listening for a new message. I included +handling for ``CancelledError``, which is how our application will +signal to this async function that the app is shutting down. + +There are many decisions that one can make here about how to deal +with errors: for example, you might perhaps +choose to terminate a connection if a particular payload fails to +deserialise properly. It seems unlikely that a client would send +through only a few invalid JSON messages, but the rest valid. For +simplicity, we'll keep what we have for now, and move onto + +Sending A Message +^^^^^^^^^^^^^^^^^ + +This time, we use a ``StreamWriter`` instance. The code below is +very similar to what we saw in the receiver: + +.. code-block:: python3 + :caption: Sending JSON Messages over a Stream + :linenos: + + from asyncio import StreamWriter + from typing import Dict + import json + + async def send_message(writer: StreamWriter, message: Dict): + payload = json.dumps(message).encode() + size_prefix = len(payload).to_bytes(3, byteorder='little') + writer.write(size_prefix) + writer.write(payload) + await writer.drain() + +Let's step through the lines: + +- line 5: This async function must be called for each message that + must be sent. +- line 6: Serialize the message to bytes. +- line 7: Build the size header; remember, this needs to be sent + before the payload itself. +- line 8: Write the header to the stream +- line 9: Write the payload to the stream. Note that because there + is no ``await`` keyword between sending the header and the payload, + we can be sure that there will be no "context switch" between + different async function calls trying to write data to this stream. +- line 10: Finally, wait for all the bytes to be sent. + +We can place the two async functions above, ``new_messages()`` +and ``send_message()``, into their own module called ``utils.py`` +(Since we'll be using these function in both our server code and +our client code!). + +For completeness, here is the utils module: + +.. literalinclude:: utils01.py + :caption: utils.py + :language: python + +Let's return to the main server application and see how to +incorporate our new utility functions into the code. + +Server: Message Handling +------------------------ TODO Notes: -- using the streams API -- first show the client. -- will have to explain a message protocol. (But that's easy) - then show the server - spend some time on clean shutdown. diff --git a/Doc/library/asyncio-tutorial/server03.py b/Doc/library/asyncio-tutorial/server03.py new file mode 100644 index 00000000000000..f2586305784228 --- /dev/null +++ b/Doc/library/asyncio-tutorial/server03.py @@ -0,0 +1,41 @@ +import asyncio +from asyncio import StreamReader, StreamWriter +from typing import Dict, Callable +from utils import new_messages + + +async def main(): + server = await asyncio.start_server( + client_connected_cb=client_connected, + host='localhost', + port='9011', + ) + async with server: + await server.serve_forever() + + +async def client_connected(reader: StreamReader, writer: StreamWriter): + handlers: Dict[str, Callable] = dict( + connect=lambda msg: print(msg.get('username')), + joinroom=lambda msg: print('joining room:', msg.get('username')), + leaveroom=lambda msg: print('leaving room:', msg.get('username')), + chat=lambda msg: print( + f'chat sent to room {msg.get("room")}: {msg.get("message")}'), + ) + + async for msg in new_messages(reader): + print('Received message:', msg) + action = msg.get('action') + if not action: + continue + + handler = handlers.get(action) + if not handler: + continue + + handler(msg) + + +if __name__ == '__main__': + asyncio.run(main()) + diff --git a/Doc/library/asyncio-tutorial/utils01.py b/Doc/library/asyncio-tutorial/utils01.py new file mode 100644 index 00000000000000..8ab7f0d5d8211a --- /dev/null +++ b/Doc/library/asyncio-tutorial/utils01.py @@ -0,0 +1,29 @@ +import json +from asyncio import ( + StreamReader, StreamWriter, IncompleteReadError, CancelledError +) +from typing import AsyncGenerator, Dict + + +async def new_messages(reader: StreamReader) -> AsyncGenerator[Dict, None]: + try: + while True: + size_prefix = await reader.readexactly(3) + size = int.from_bytes(size_prefix, byteorder='little') + message = await reader.readexactly(size) + try: + yield json.loads(message) + except json.decoder.JSONDecodeError: + continue + except (IncompleteReadError, ConnectionAbortedError, + ConnectionResetError, CancelledError): + # The connection is dead, leave. + return + + +async def send_message(writer: StreamWriter, message: Dict): + payload = json.dumps(message).encode() + size_prefix = len(payload).to_bytes(3, byteorder='little') + writer.write(size_prefix) + writer.write(payload) + await writer.drain() From 7f2f14962fe4d7aa590e3a0323f1438d64b6f20d Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 21 Oct 2018 17:05:04 +1000 Subject: [PATCH 06/31] Add message receiving to server code. --- .../case-study-chat-server.rst | 39 +++++++++++++++++-- Doc/library/asyncio-tutorial/server03.py | 23 ++++++++--- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/Doc/library/asyncio-tutorial/case-study-chat-server.rst b/Doc/library/asyncio-tutorial/case-study-chat-server.rst index 4a4ec06e0c50fe..74750aafdc2215 100644 --- a/Doc/library/asyncio-tutorial/case-study-chat-server.rst +++ b/Doc/library/asyncio-tutorial/case-study-chat-server.rst @@ -24,7 +24,7 @@ run for a long time, serving clients that may connect, disconnect, and then later reconnect multiple times. .. literalinclude:: server01.py - :language: python + :language: python3 As explained earlier, ``main`` itself is a *coroutine function*, and when evaluated, i.e., ``main()``, it returns a *coroutine* object @@ -50,7 +50,7 @@ We can use the *Streams API* (ref:TODO) to create a TCP server very easily: .. literalinclude:: server02.py - :language: python + :language: python3 :linenos: We've added the ``start_server()`` call on line 5, and this call takes @@ -285,7 +285,7 @@ For completeness, here is the utils module: .. literalinclude:: utils01.py :caption: utils.py - :language: python + :language: python3 Let's return to the main server application and see how to incorporate our new utility functions into the code. @@ -293,6 +293,39 @@ incorporate our new utility functions into the code. Server: Message Handling ------------------------ +Below, we import the new ``utils.py`` module and incorporate +some handling for receiving new messages: + +.. literalinclude:: server03.py + :caption: Server code with basic message handling + :linenos: + :language: python3 + +We've added a few new things inside the ``client_connected`` +callback function: + +- line 19: This is a handler function that will get called if a "connect" + message is received. We have similar handler functions for the other + action types. +- line 31: A simple dictionary that maps an action "name" to the handler + function for that action. +- line 38: Here you can see how our async generator ``new_messages()`` + gets used. We simply loop over it, as you would with any other generator, + and it will return a message only when one is received. Note the one + minor difference as compared to a regular generator: you have to iterate + over an async generator with ``async for``. +- line 39: Upon receiving a message, check which action must be taken. +- line 43: Look up the *handler function* that corresponds to the + action. We set up these handlers earlier at line 31. +- line 47: call the handler function. + +Our server code still doesn't do much; but at least it'll be testable +with a client sending a few different kinds of actions, and we'll be +able to see print output for each different kind of action received. + +The next thing we'll have to do is set up chat rooms. There's no point +receiving messages if there's nowhere to put them! + TODO Notes: diff --git a/Doc/library/asyncio-tutorial/server03.py b/Doc/library/asyncio-tutorial/server03.py index f2586305784228..35fa29dab182fc 100644 --- a/Doc/library/asyncio-tutorial/server03.py +++ b/Doc/library/asyncio-tutorial/server03.py @@ -15,16 +15,27 @@ async def main(): async def client_connected(reader: StreamReader, writer: StreamWriter): + + def connect(msg): + print(msg.get('username')) + + def joinroom(msg): + print('joining room:', msg.get('username')) + + def leaveroom(msg): + print('leaving room:', msg.get('username')) + + def chat(msg): + print(f'chat sent to room {msg.get("room")}: {msg.get("message")}') + handlers: Dict[str, Callable] = dict( - connect=lambda msg: print(msg.get('username')), - joinroom=lambda msg: print('joining room:', msg.get('username')), - leaveroom=lambda msg: print('leaving room:', msg.get('username')), - chat=lambda msg: print( - f'chat sent to room {msg.get("room")}: {msg.get("message")}'), + connect=connect, + joinroom=joinroom, + leaveroom=leaveroom, + chat=chat, ) async for msg in new_messages(reader): - print('Received message:', msg) action = msg.get('action') if not action: continue From 61402e1352b7eaec2a6e3fb231ac379628e03119 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 21 Oct 2018 18:49:48 +1000 Subject: [PATCH 07/31] Added skeleton suggestions for the cookbook section --- .../asyncio-tutorial/asyncio-cookbook.rst | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/Doc/library/asyncio-tutorial/asyncio-cookbook.rst b/Doc/library/asyncio-tutorial/asyncio-cookbook.rst index fd4979ceedd37d..7a1b2d59e33f1c 100644 --- a/Doc/library/asyncio-tutorial/asyncio-cookbook.rst +++ b/Doc/library/asyncio-tutorial/asyncio-cookbook.rst @@ -4,8 +4,215 @@ Asyncio Cookbook Let's look at a few common situations that will come up in your ``asyncio`` programs, and how best to tackle them. +[There's a lot more we can do if we're able to refer to +3rd party packages here. We could show a websockets example, +and other things.] + +Using A Queue To Move Data Between Long-Lived Tasks +--------------------------------------------------- + TODO +Using A Queue To Control A Pool of Resources +-------------------------------------------- + +- show example with a pool of workers +- show example with a connection pool + +Keeping Track Of Many Connections +--------------------------------- + +- example using a global dict +- show how a weakref container can simplify cleanup +- show how to access connection info e.g. ``get_extra_info()`` +- this kind of thing: + +.. code-block:: python3 + + import asyncio + from weakref import WeakValueDictionary + + CONNECTIONS = WeakValueDictionary() + + async def client_connected_cb(reader, writer): + + addr = writer.get_extra_info('peername') + print(f'New connection from {addr}') + + # Every new connection gets added to the global dict. + # Actually, *writer* objects get added. This makes + # it easy to look up a connection and immediately + # send data to it from other async functions. + CONNECTIONS[addr] = writer + ... + + async def main(): + server = await asyncio.start_server( + client_connected_cb=client_connected_db, + host='localhost', + port='9011', + ) + async with server: + await server.serve_forever() + + if __name__ == '__main__': + asyncio.run(main()) + +Handling Reconnection +--------------------- + +- Example is a client app that needs to reconnect to a server + if the server goes down, restarts, or there is a network partition + or other general kind of error + +Async File I/O +-------------- + +- mention that disk I/O is still IO +- Python file operations like ``open()``, etc. are blocking +- I think all we can do here is refer to the 3rd party *aiofiles* + package? +- I suppose we could show how to do file IO in thread, driven + by ``run_in_executor()``... + +Wait For Async Results In Parallel +---------------------------------- + +TODO + +- show an example with gather +- show another example with wait +- maybe throw in an example with gather that also uses + "wait_for" for timeout +- either include "return_exceptions" here or in a different question + +.. code-block:: python3 + + import asyncio + + async def slow_sum(x, y): + result = x + y + await asyncio.sleep(result) + return result + + async def main(): + results = await asyncio.gather( + slow_sum(1, 1), + slow_sum(2, 2), + ) + print(results) # "[2, 4]" + + if __name__ == '__main__': + asyncio.run(main()) + +Secure Client-Server Networking +------------------------------- + +- built-in support for secure sockets +- you have to make your own secret key, and server certificate + +.. code-block:: bash + :caption: Create a new private key and certificate + + $ openssl req -newkey rsa:2048 -nodes -keyout chat.key \ + -x509 -days 365 -out chat.crt + +This creates ``chat.key`` and ``chat.crt`` in the current dir. + +.. code-block:: python3 + :caption: Secure server + + import asyncio + import ssl + + async def main(): + ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ctx.check_hostname = False + + # These must have been created earlier with openssl + ctx.load_cert_chain('chat.crt', 'chat.key') + + server = await asyncio.start_server( + client_connected_cb=client_connected_cb, + host='localhost', + port=9011, + ssl=ctx, + ) + async with server: + await server.serve_forever() + + async def client_connected_cb(reader, writer): + print('Client connected') + received = await reader.read(1024) + while received: + print(f'received: {received}') + received = await reader.read(1024) + + if __name__ == '__main__': + asyncio.run(main()) + + +.. code-block:: python3 + :caption: Secure client + + import asyncio + import ssl + + async def main(): + print('Connecting...') + ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + ctx.check_hostname = False + + # The client must only have access to the cert *not* the key + ctx.load_verify_locations('chat.crt') + reader, writer = await asyncio.open_connection( + host='localhost', + port=9011, + ssl=ctx + ) + + writer.write(b'blah blah blah') + await writer.drain() + writer.close() + await writer.wait_closed() + + if __name__ == '__main__': + asyncio.run(main()) + +Correctly Closing Connections +----------------------------- + +- from the client side +- from the server side + +Handling Typical Socket Errors +------------------------------ + +- Maybe describe the situations in which they can occur? Not sure. + +- ``ConnectionError`` +- ``ConnectionResetError`` +- ``ConnectionAbortedError`` +- ``ConnectionRefusedError`` + +Might also want to show some examples of ``asyncio.IncompleteReadError``. + +Graceful Shutdown on Windows +---------------------------- + +TODO + + +Run A Blocking Call In An Executor +---------------------------------- + +- show example with default executor +- show example with a custom executor (thread-based) +- show example with a custom executor (process-based) + + + + Notes: - My thinking here was a Q&A style, and then each section has From 550bdbf43ae9cc41eecaa631822a2c9ec572caa9 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 21 Oct 2018 19:15:31 +1000 Subject: [PATCH 08/31] Further notes in the cookbook --- .../asyncio-tutorial/asyncio-cookbook.rst | 25 +++++++++++++++ Doc/library/asyncio-tutorial/what-asyncio.rst | 32 ++++++++++++------- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/Doc/library/asyncio-tutorial/asyncio-cookbook.rst b/Doc/library/asyncio-tutorial/asyncio-cookbook.rst index 7a1b2d59e33f1c..afec564be2ac4b 100644 --- a/Doc/library/asyncio-tutorial/asyncio-cookbook.rst +++ b/Doc/library/asyncio-tutorial/asyncio-cookbook.rst @@ -19,6 +19,26 @@ Using A Queue To Control A Pool of Resources - show example with a pool of workers - show example with a connection pool +Best Practices For Timeouts +--------------------------- + +- start with ``asyncio.wait_for()`` +- also look at ``asyncio.wait()``, and what to do if not all tasks + are finished when the timeout happens. Also look at the different + termination conditions of ``asyncio.wait()`` + +How To Handle Cancellation +-------------------------- + +- app shutdown +- how to handle CancelledError and then close sockets +- also, when waiting in a loop on ``await queue.get()`` is it better to + handle CancelledError, or use the idiom of putting ``None`` on the + queue? (``None`` would be better because it ensures the contents of the + queue get processed first, but I don't think we can prevent + CancelledError from getting raised so it must be handled anyway. I + can make an example to explain better.) + Keeping Track Of Many Connections --------------------------------- @@ -105,6 +125,9 @@ TODO if __name__ == '__main__': asyncio.run(main()) +- we should also include a brief discussion of "when to use asyncio.gather and + when to use asyncio.wait" + Secure Client-Server Networking ------------------------------- @@ -197,6 +220,8 @@ Handling Typical Socket Errors Might also want to show some examples of ``asyncio.IncompleteReadError``. +Also link/refer to the socket programming HOWTO in the docs. + Graceful Shutdown on Windows ---------------------------- diff --git a/Doc/library/asyncio-tutorial/what-asyncio.rst b/Doc/library/asyncio-tutorial/what-asyncio.rst index f0b58cce3148f0..d9e3f453b1d4fe 100644 --- a/Doc/library/asyncio-tutorial/what-asyncio.rst +++ b/Doc/library/asyncio-tutorial/what-asyncio.rst @@ -51,7 +51,7 @@ finish before the second call (to ``host2``) can begin. Computers process things sequentially, which is why programming languages like Python also work sequentially; but what we see here is that our code is also going to **wait** sequentially. This is, quite literally, -a waste of time. What we would really like to do here is wait for the +a waste of time. What we would really like to do here is wait for all the replies *concurrently*, i.e., at the same time. Preemptive Concurrency @@ -64,7 +64,8 @@ time on your device. At least, they will appear to be running concurrently. What is really happening is that the operating system is sharing little slices of processor (CPU) time among all the processes. If we started two copies of our ``greet`` program at the -same time, then they *would* run (and therefore wait) concurrently. +same time, they *would* run (and therefore wait) concurrently which is +exactly what we want. However, there is a price for that: each new process consumes resources from the operating system. But more than that, there is another tricky @@ -79,14 +80,19 @@ task being carried out by a computer system, without requiring its cooperation, and with the intention of resuming the task at a later time*. -This means that you will never be sure of when each of your processes -and threads is *actually* executing on a CPU. For processes, this -is quite safe because +Operating Systems do this kind of preemptive switching for both +processes and threads. A simplistic but useful description of the +difference is that one process can have multiple threads, and those +threads share all the memory in their parent process. + +Because of this preemptive switching, you will never be sure of +when each of your processes and threads is *actually* executing on +a CPU. For processes, this is quite safe because their memory spaces are isolated from each other; however, -**threads** are not isolated from each other. In fact, the primary -feature of threads over processes is that multiple threads within a -single process can access the same memory. And this is where all the -problems begin. +**threads** are not isolated from each other (within the same process). +In fact, the primary feature of threads over processes is that +multiple threads within a single process can access the same memory. +And this is where all the problems begin. Jumping back to our code sample further up: we may also choose to run the ``greet()`` function in multiple threads; and then @@ -96,7 +102,7 @@ with no control over how execution will be transferred between the two threads (unless you use the synchronization primitives in the ``threading`` module) . This situation can result in *race conditions* in how objects are modified, -and these bugs can be very difficult to debug. +and these bugs can be very difficult to fix. Cooperative Concurrency ----------------------- @@ -106,11 +112,11 @@ multiple socket connections all in a single thread; and the best part is that you get to control *when* execution is allowed to switch between these different contexts. -We will explain more of the details later on in the tutorial, +We will explain more of the details throughout this tutorial, but briefly, our earlier example becomes something like the following pseudocode: -.. code-block:: python +.. code-block:: python3 import asyncio @@ -123,6 +129,8 @@ pseudocode: print('Reply:', repr(reply)) async def main(): + + # Both calls run at the same time await asyncio.gather( greet(host1, port1), greet(host2, port2) From e7bc56d4b52f1ded7976bc1b012212e6ef8f34ff Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 4 Nov 2018 15:52:28 +1000 Subject: [PATCH 09/31] Further work on describing how async def functions work --- .../asyncio-tutorial/async-functions.rst | 271 +++++++++++++++++- .../asyncio-tutorial/asyncio-cookbook.rst | 13 + Doc/library/asyncio-tutorial/what-asyncio.rst | 4 +- 3 files changed, 282 insertions(+), 6 deletions(-) diff --git a/Doc/library/asyncio-tutorial/async-functions.rst b/Doc/library/asyncio-tutorial/async-functions.rst index 89d0f57040ab06..8070b4cb94c6db 100644 --- a/Doc/library/asyncio-tutorial/async-functions.rst +++ b/Doc/library/asyncio-tutorial/async-functions.rst @@ -4,7 +4,7 @@ Functions: Sync vs Async Regular Python functions are created with the keyword ``def``, and look like this: -.. code-block:: python +.. code-block:: python3 def f(x, y): print(x + y) @@ -14,7 +14,7 @@ and look like this: Async functions are different in two respects: -.. code-block:: python +.. code-block:: python3 import asyncio @@ -39,7 +39,7 @@ way. That means the following: -.. code-block:: python +.. code-block:: python3 import asyncio @@ -52,10 +52,273 @@ That means the following: asyncio.run(main()) -The execution of the first async function, ``main()``, is performed +The execution of the the async function ``main()`` is performed with ``run()``, but once you're inside an ``async def`` function, then all you need to execute another async function is the ``await`` keyword. +Ordinary (Sync) Functions Cannot Await Async Functions +------------------------------------------------------ + +If you're starting inside a normal function, you cannot call an +async function using the ``await`` keyword. Doing so will produce +a syntax error: + +.. code-block:: python3 + + >>> async def f(x, y): + ... pass + ... + >>> def g(): + ... await f(1, 2) + ... + File "", line 2 + SyntaxError: 'await' outside async function + +To fix this, either ``asyncio.run()`` must be used, or the function +``g`` must be changed to be an ``async def`` function. The ``run()`` +function is tricky: you can't call it in a nested way, because +internally it creates an event loop and you cannot have two event +loops running at the same time (in the same thread). So the following +is also illegal: + +.. code-block:: python3 + + >>> import asyncio + >>> async def f(x, y): + ... pass + ... + >>> def g(): + ... asyncio.run(f(1, 2)) + ... + >>> async def main(): + ... g() + ... + >>> asyncio.run(main()) + Traceback (most recent call last): + + + + File "G:\Programs\Python37\lib\asyncio\runners.py", line 34, in run + "asyncio.run() cannot be called from a running event loop") + RuntimeError: asyncio.run() cannot be called from a running event loop + +So ``asyncio.run()`` is really intended only for launching your *first* +async function; after that, every other async function should be +executed using the ``await`` keyword, and the task-based methods which +we've not yet discussed. + +Async Functions Can Call Sync Functions +--------------------------------------- + +The inverse works perfectly fine: calling ordinary Python functions +from inside ``async def`` functions. Here's an example: + +.. code-block:: python3 + + >>> import asyncio + >>> import time + >>> async def f(): + ... print(time.ctime()) + ... + >>> asyncio.run(f()) + Sun Nov 4 15:04:45 2018 + +Accurate Terminology For Async Functions +---------------------------------------- + +So far in this tutorial we've been intentionally sloppy with how +we refer to things like *async functions* or *async def* functions, +and *normal Python functions* and so on. It's time to get more +specific about what to call each of these things. It's important +because we need to be able to understand the difference between +a **coroutine** and a **coroutine function**, and a few other things +still to be introduced. + +So let's do that now, using the ``inspect`` module. First let's look +at the two kinds of functions: + +.. code-block:: python3 + + >>> import inspect + >>> def f1(): + ... pass + ... + >>> inspect.isfunction(f1) + True + >>> inspect.iscoroutinefunction(f1) + False + +This is an ordinary Python function, and the ``inspect`` module +confirms that, but we've included another test to see if the function +is a *coroutine function*, which is ``False`` as expected. Let's do +the same on an ``async def`` function: + +.. code-block:: python3 + + >>> async def f2(): + ... pass + ... + >>> inspect.isfunction(f2) + True + >>> inspect.iscoroutinefunction(f2) + True + +According to Python, ``f2`` is also considered to be a function, but +more specifically, it is a *coroutine function*, and this is the +specific name we will be using for *async def* functions. + +Why does it matter? Well, when you evaluate a coroutine function, it'll +return something: + +.. code-block:: python3 + + >>> async def f2(): + ... pass + ... + >>> result = f2() + >>> type(result) + + >>> inspect.iscoroutine(result) + True + +The point we're trying to make here is that an *async def* function +is not yet a coroutine, but rather only a *coroutine function*; only +when you *evaluate* the coroutine function, will a coroutine +object be returned. The ``await`` keyword, which we showed in +previous examples, is acting on *coroutine* objects, not +the coroutine functions that create them. + +This can be made clear in the following example: + +.. code-block:: python3 + + >>> async def f3(): + ... return 123 + ... + >>> async def main(): + ... obj = f3() + ... result = await obj + ... print(result) + ... + >>> asyncio.run(main()) + 123 + +In the code above, the value of ``obj`` is *not* ``123`` when +coroutine function ``f3`` is evaluated. Instead, ``obj`` is a +*coroutine* object, and it will only get executed when the +``await`` keyword is used. Of course, you don't have to write +code like this where you first get the coroutine and then +use ``await`` on the object; simply evaluate the +coroutine function and use ``await`` all in the same line. + +An Aside: Similarity To Generator Functions +------------------------------------------- + +This has nothing to do with asyncio, but you might be interested +to see how this difference between a function and a +coroutine function is quite similar to the difference between +functions and generator functions: + +.. code-block:: python3 + + >>> def g(): + ... yield 123 + ... + >>> inspect.isfunction(g) + True + >>> inspect.isgeneratorfunction(g) + True + +If a function uses the ``yield`` keyword anywhere inside the function +body, that function becomes a *generator function*, very similar to +how a function declared with ``async def`` becomes a +*coroutine function*. And, completing the comparison, if you +evaluate a generator function, a *generator* object is returned, similar +to how a coroutine function, when evaluated, returns a coroutine +object: + +.. code-block:: python3 + + >>> def g(): + ... yield 123 + ... + >>> obj = g() + >>> type(obj) + + >>> inspect.isgenerator(obj) + True + +Again, this doesn't have anything to do with asyncio, but +the loose similarity between generator functions and +coroutine functions might give you a useful framework for understanding +the new coroutine functions. + +Terminology For Async Generators +-------------------------------- + +The previous section was useful for giving you a basic framework +for understanding how coroutines and generator have similar +characteristics. Here, we show how we can also make asynchronous +generator functions! It sounds much more complicated than it +really is, so let's jump directly to some examples: + +.. code-block:: python3 + + >>> import asyncio + >>> async def ag(): + ... yield 123 + ... + >>> async def main(): + ... async for value in ag(): + ... print(value) + ... + >>> asyncio.run(main()) + 123 + +If you pretend for a second that the word "async" is temporarily +removed from the code above, the behaviour of the generator +should look very familiar to you (assuming you already know how +Python's generators work). The generator function yields out +values and these values are obtained by iterating over the +generator. + +The difference now is of course the presence of those "async" +words. The code sample doesn't a good reason *why* an async +generator is being used here. That will come later in the +cookbook. All we want to discuss here is what these kinds of +functions and objects should be called. + +Let's have a close look at the function `ag`: + +.. code-block:: python3 + + >>> async def ag(): + ... yield 123 + ... + >>> inspect.isfunction(ag) + True + + # Ok, so it's a function... + + >>> inspect.iscoroutinefunction(ag) + False + + # ...but it's not a coroutine function, despite "async def" + + >>> inspect.isasyncgenfunction(ag) + True + + # Aha, so this is an "async generator function"... + + >>> inspect.isasyncgen(ag()) + True + + # ...and when evaluated, it returns an "async generator" + + + + + TODO: - which kind of functions can be called from which other kind - use the "inspect" module to verify the formal names of functions, diff --git a/Doc/library/asyncio-tutorial/asyncio-cookbook.rst b/Doc/library/asyncio-tutorial/asyncio-cookbook.rst index afec564be2ac4b..935f672d34edf7 100644 --- a/Doc/library/asyncio-tutorial/asyncio-cookbook.rst +++ b/Doc/library/asyncio-tutorial/asyncio-cookbook.rst @@ -207,6 +207,8 @@ Correctly Closing Connections - from the client side - from the server side +- Yury I need your help here. What is the actual "correct" way + to do this? Streams API preferable, if possible. Handling Typical Socket Errors ------------------------------ @@ -236,6 +238,17 @@ Run A Blocking Call In An Executor - show example with a custom executor (process-based) +Adding Asyncio To An Existing Sync (Threaded) Application +--------------------------------------------------------- + +- Imagine an existing app that uses threading for concurrency, + but we want to make use of asyncio only for, say, a large + number of concurrent GET requests, but leave the rest of the + app unchanged. +- Plan would be to run the asyncio loop in another thread +- Can show how to safely communicate between that thread and + the main thread (or others). + Notes: diff --git a/Doc/library/asyncio-tutorial/what-asyncio.rst b/Doc/library/asyncio-tutorial/what-asyncio.rst index d9e3f453b1d4fe..ffc4269d90f94c 100644 --- a/Doc/library/asyncio-tutorial/what-asyncio.rst +++ b/Doc/library/asyncio-tutorial/what-asyncio.rst @@ -3,7 +3,7 @@ What Does "Async" Mean? Let's make a function that communicates over the network: -.. code-block:: python +.. code-block:: python3 import socket @@ -30,7 +30,7 @@ Python only ever executes one line at a time. Now the question comes up: what if you need to send a greeting to *multiple* hosts? You could just call it twice, right? -.. code-block:: python +.. code-block:: python3 import socket From 3d4cdaee6a296a24ec6794b466b052fb6d3f88b5 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sat, 15 Jun 2019 12:14:57 +1000 Subject: [PATCH 10/31] Fix review comment from @tirkarthi --- Doc/library/asyncio-tutorial/async-functions.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/asyncio-tutorial/async-functions.rst b/Doc/library/asyncio-tutorial/async-functions.rst index 8070b4cb94c6db..4f631b7f491b7e 100644 --- a/Doc/library/asyncio-tutorial/async-functions.rst +++ b/Doc/library/asyncio-tutorial/async-functions.rst @@ -283,8 +283,8 @@ values and these values are obtained by iterating over the generator. The difference now is of course the presence of those "async" -words. The code sample doesn't a good reason *why* an async -generator is being used here. That will come later in the +words. The code sample doesn't show a good reason *why* an async +generator is being used here: that will come later in the cookbook. All we want to discuss here is what these kinds of functions and objects should be called. From e0bb48b62e183ecf82d9f74e12297bce6aad93f9 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sat, 15 Jun 2019 13:27:42 +1000 Subject: [PATCH 11/31] Fix typo --- Doc/library/asyncio-tutorial/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/asyncio-tutorial/index.rst b/Doc/library/asyncio-tutorial/index.rst index ee4d958aa91ffd..30671bb5a449da 100644 --- a/Doc/library/asyncio-tutorial/index.rst +++ b/Doc/library/asyncio-tutorial/index.rst @@ -1,7 +1,7 @@ Asyncio Tutorial ================ -Programming with ``async def`` functions is differs from normal Python +Programming with ``async def`` functions is different to normal Python functions; enough so that it is useful to explain a bit more about what ``asyncio`` is for, and how to use it in typical programs. From 5e4550af550542adf097441848f818392c912975 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sat, 15 Jun 2019 13:28:15 +1000 Subject: [PATCH 12/31] Clarify the "What is async" section --- Doc/library/asyncio-tutorial/what-asyncio.rst | 59 +++++++++++-------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/Doc/library/asyncio-tutorial/what-asyncio.rst b/Doc/library/asyncio-tutorial/what-asyncio.rst index ffc4269d90f94c..c59050a203428a 100644 --- a/Doc/library/asyncio-tutorial/what-asyncio.rst +++ b/Doc/library/asyncio-tutorial/what-asyncio.rst @@ -18,14 +18,14 @@ Let's make a function that communicates over the network: This function will: #. make a socket connection to a host, -#. send a greeting, and -#. wait for a reply. +#. send ``b'Hello, world```, and +#. **wait** for a reply. -The key point here is about the word *wait*: execution proceeds line-by-line -until the line ``reply = s.recv(1024)``. We call this behaviour -*synchronous*. At this point, execution pauses -until we get a reply from the host. This is as we expect: -Python only ever executes one line at a time. +The key point here is about the word *wait*: in the code, execution proceeds line-by-line +until the line ``reply = s.recv(1024)``. While these lines +of code are executing, no other code (in the same thread) can run. We call this behaviour +*synchronous*. At the point where we wait, execution pauses +until we get a reply from the host. Now the question comes up: what if you need to send a greeting to *multiple* hosts? You could just call it twice, right? @@ -58,22 +58,23 @@ Preemptive Concurrency ---------------------- Operating systems like Windows, Mac and Linux, and others, understand -this problem deeply. If you're reading this on a computer or even your +this problem deeply. If you're reading this on a computer or even on your mobile, there will be tens or hundreds of processes running at the same time on your device. At least, they will appear to be running concurrently. What is really happening is that the operating system is sharing little slices of processor (CPU) time among all the -processes. If we started two copies of our ``greet`` program at the -same time, they *would* run (and therefore wait) concurrently which is -exactly what we want. +processes. If we started two completely separate instances of our ``greet`` program at the same time, they *would* run (and therefore wait) +concurrently which is exactly what we want. -However, there is a price for that: each new process consumes resources -from the operating system. But more than that, there is another tricky +However, there is a price for that: each new process consumes extra resources +from the operating system; it's not just that we wait in parallel, but +*everything* is now in parallel, a full copy of our program. +But more than that, there is another tricky problem about *how* the operating system knows when to allocate -execution time between each process. The answer: it doesn't! This means -that the operating system can decide when to give processor time to each -process. Your code, and therefore you, will not know when these switches -occur. This is called "preemption". From +execution time between each copy of our process. The answer: it doesn't! +This means that the operating system can decide when to give processor +time to each process. Your code, and therefore you, will not know when +these switches occur. This is called "preemption". From `Wikipedia `_: *In computing, preemption is the act of temporarily interrupting a task being carried out by a computer system, without requiring @@ -87,7 +88,7 @@ threads share all the memory in their parent process. Because of this preemptive switching, you will never be sure of when each of your processes and threads is *actually* executing on -a CPU. For processes, this is quite safe because +a CPU *relative to each other*. For processes, this is quite safe because their memory spaces are isolated from each other; however, **threads** are not isolated from each other (within the same process). In fact, the primary feature of threads over processes is that @@ -98,8 +99,8 @@ Jumping back to our code sample further up: we may also choose to run the ``greet()`` function in multiple threads; and then they will also wait for replies concurrently. However, now you have two threads that are allowed to access the same objects in memory, -with no control over -how execution will be transferred between the two threads (unless you +with little control over +how execution will switch between the two threads (unless you use the synchronization primitives in the ``threading`` module) . This situation can result in *race conditions* in how objects are modified, and these bugs can be very difficult to fix. @@ -129,7 +130,6 @@ pseudocode: print('Reply:', repr(reply)) async def main(): - # Both calls run at the same time await asyncio.gather( greet(host1, port1), @@ -138,11 +138,24 @@ pseudocode: asyncio.run(main()) +In this code, the two instances of the ``greet()`` function will +run concurrently. + There are a couple of new things here, but I want you to focus on the new keyword ``await``. Unlike threads, execution is allowed to switch between the two ``greet()`` invocations **only** where the ``await`` keyword appears. On all other lines, execution is exactly the -same as normal Python. These ``async def`` functions are called +same as normal Python, and will not be preempt by thread switching (there's +typically only a single thread in most ``asyncio`` programs). +These ``async def`` functions are called "asynchronous" because execution does not pass through the function top-down, but instead can suspend in the middle of a function at the -``await`` keyword, and allow another function to execute. +``await`` keyword, and allow another function to execute while +*this function* is waiting for network data. + +An additional advantage of the *async* style above is that it lets us +manage several thousand concurrent long-lived socket connections in a simple way. +One can also use threads to manage concurrent long-lived socket connections, +but it gets difficult to go past a few thousand because the creation +of operating system threads, just like processes, consumes additional +resources from the operating system. From 0de27489edb84bbb768f01689b132783cbe20c13 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sat, 15 Jun 2019 13:29:12 +1000 Subject: [PATCH 13/31] Flesh out the sync-versus-async functions section --- .../asyncio-tutorial/async-functions.rst | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/Doc/library/asyncio-tutorial/async-functions.rst b/Doc/library/asyncio-tutorial/async-functions.rst index 4f631b7f491b7e..4fa8fa046c2c87 100644 --- a/Doc/library/asyncio-tutorial/async-functions.rst +++ b/Doc/library/asyncio-tutorial/async-functions.rst @@ -35,7 +35,8 @@ Executing Async Functions The ``run`` function is only good for executing an async function from "synchronous" code; and this is usually only used to execute a "main" async function, from which others can be called in a simpler -way. +way. You will not see ``asyncio.run()`` being called from inside an +``async def`` function. That means the following: @@ -54,7 +55,7 @@ That means the following: The execution of the the async function ``main()`` is performed with ``run()``, but once you're inside an ``async def`` function, then -all you need to execute another async function is the ``await`` keyword. +calling another async function is done with the ``await`` keyword. Ordinary (Sync) Functions Cannot Await Async Functions ------------------------------------------------------ @@ -104,7 +105,7 @@ is also illegal: So ``asyncio.run()`` is really intended only for launching your *first* async function; after that, every other async function should be -executed using the ``await`` keyword, and the task-based methods which +executed using the ``await`` keyword, and the task-based strategies which we've not yet discussed. Async Functions Can Call Sync Functions @@ -123,6 +124,42 @@ from inside ``async def`` functions. Here's an example: >>> asyncio.run(f()) Sun Nov 4 15:04:45 2018 +One of the benefits of ``asyncio`` is that you can see at a glance +which code inside a function is subject to a context switch. In the +following code example, we have two kinds of ``sleep()``: a blocking +version from the ``time`` module, and an async version from ``asyncio``: + +.. code-block:: python3 + + >>> import time, asyncio + >>> def func1(): + ... time.sleep(0) + ... + >>> async def func2(): + ... await asyncio.sleep(0) + ... + >>> async def main(): + ... await func2() # (1) + ... func1() + ... func1() + ... func1() + ... func1() + ... func1() + ... func1() + ... await func2() # (2) + ... + >>> asyncio.run(main()) + +At (1), the underlying event loop is given the opportunity to switch from +``main()`` to any other tasks that are waiting to run, and after line (1) +returns, a series of calls to the sync function ``func1()`` occurs before +the next allowable context switch on the event loop at (2). While the +series of sync calls are running, *no other code* will execute in the +current thread, until you get to the next ``await``. This guarantee applies +a dramatic simplifying effect on your code, because now you can modify +data shared between multiple async tasks without fear of introducing +a race condition. + Accurate Terminology For Async Functions ---------------------------------------- From 89364f861ba8807e57f5f102270a98c93f70db80 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sat, 15 Jun 2019 16:21:18 +1000 Subject: [PATCH 14/31] Add the blurb entry --- .../next/Documentation/2019-06-15-14-58-28.bpo-34831.mFkyqe.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Documentation/2019-06-15-14-58-28.bpo-34831.mFkyqe.rst diff --git a/Misc/NEWS.d/next/Documentation/2019-06-15-14-58-28.bpo-34831.mFkyqe.rst b/Misc/NEWS.d/next/Documentation/2019-06-15-14-58-28.bpo-34831.mFkyqe.rst new file mode 100644 index 00000000000000..71ae600ad89544 --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2019-06-15-14-58-28.bpo-34831.mFkyqe.rst @@ -0,0 +1 @@ +Add asyncio tutorial From be474f460c36af063b93da5ca1ebc411bccc192b Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sat, 15 Jun 2019 16:21:43 +1000 Subject: [PATCH 15/31] Remove TODOs --- Doc/library/asyncio-tutorial/async-functions.rst | 11 ----------- .../asyncio-tutorial/case-study-chat-server.rst | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/Doc/library/asyncio-tutorial/async-functions.rst b/Doc/library/asyncio-tutorial/async-functions.rst index 4fa8fa046c2c87..9008c586f13f4f 100644 --- a/Doc/library/asyncio-tutorial/async-functions.rst +++ b/Doc/library/asyncio-tutorial/async-functions.rst @@ -351,14 +351,3 @@ Let's have a close look at the function `ag`: True # ...and when evaluated, it returns an "async generator" - - - - - -TODO: -- which kind of functions can be called from which other kind -- use the "inspect" module to verify the formal names of functions, -coroutine functions, coroutines, etc. - - diff --git a/Doc/library/asyncio-tutorial/case-study-chat-server.rst b/Doc/library/asyncio-tutorial/case-study-chat-server.rst index 74750aafdc2215..5d8650eb1592f3 100644 --- a/Doc/library/asyncio-tutorial/case-study-chat-server.rst +++ b/Doc/library/asyncio-tutorial/case-study-chat-server.rst @@ -46,7 +46,7 @@ the actual server. Server ------ -We can use the *Streams API* (ref:TODO) to create a TCP server very +We can use the *Streams API* to create a TCP server very easily: .. literalinclude:: server02.py From c40310126d96287608ffb96d5ea4d90bbe92c18c Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sat, 15 Jun 2019 16:22:00 +1000 Subject: [PATCH 16/31] Write "Executing Async Functions" --- .../running-async-functions.rst | 120 +++++++++++++++++- 1 file changed, 114 insertions(+), 6 deletions(-) diff --git a/Doc/library/asyncio-tutorial/running-async-functions.rst b/Doc/library/asyncio-tutorial/running-async-functions.rst index bca19d78e73bc7..fea02491181a07 100644 --- a/Doc/library/asyncio-tutorial/running-async-functions.rst +++ b/Doc/library/asyncio-tutorial/running-async-functions.rst @@ -1,11 +1,119 @@ Executing Async Functions ========================= -TODO +In a previous section we looked at the difference between sync functions +and async functions. Here we focus specifically on async functions, and +how to call them. -Notes: +Imagine we have an async function, called ``my_coro_fn``, and we want to +run it. There are three ways: -- can be called by other async functions -- can NOT be called by sync functions -- can be executed by ``asyncio.run()`` -- can be executed in task by ``asyncio.create_task()`` +1. ``asyncio.run(my_coro_fn())`` +2. ``await my_coro_fn()`` +3. ``asyncio.create_task(my_coro_fn())`` + +The first, ``asyncio.run()`` will create a new event loop and is intended +to be called only from sync code. It is typically used to start off the +whole program. + +The second, ``await my_coro_fn()``, has already been covered in a previous +section and is used to both execute the async function, and wait for the +function to complete. The ``await`` keyword can only be used inside an +``async def`` function. It is expected that *most* of your async functions +will be executed with the ``await`` keyword. + +The third is something we haven't covered before: the ``asyncio.create_task()`` +function. This function will call your async function, and create an +``asyncio.Task`` object to wrap it. This means that your async function will +begin executing but the code in the calling context will *not* wait for your +async function before continuing with the next code. + +Let's have a look at that with an example: + +.. code-block:: python3 + + import asyncio + + async def f(): + await asyncio.sleep(10) + print('f is done') + + async def g(): + await asyncio.sleep(5) + print('g is done') + + async main(): + asyncio.create_task(f()) # (1) + await g() # (2) + + asyncio.run(main()) + +Looking at line (1), we see that async function ``f()`` is called and +passed to ``create_task()``, and immediately after, async function ``g()`` +is called with ``await g()``. + +Even though ``f()`` is called first, async function ``g()`` will finish +first, and you'll see "g is done" printed before "f is done". This is because +although ``create_task()`` does schedule the given async function to be +executed, it does not wait for the call to complete, unlike when the +``await`` keyword is used. + +However, note that the task returned by ``create_task()`` can indeed be +awaited, and this will make the order of calls sequential once again: + +.. code-block:: python3 + + import asyncio + + async def f(): + await asyncio.sleep(10) + print('f is done') + + async def g(): + await asyncio.sleep(5) + print('g is done') + + async main(): + task = asyncio.create_task(f()) # (1) + await task + await g() # (2) + + asyncio.run(main()) + +In the sample above, we specifically use the ``await`` keyword on the task +object returned by the ``create_task()`` function, and this means that +the execution of that task must complete before the next ``await g()`` call +can be started. + +There are a few other ways that async functions can be started, but they +are just decoration over the three ways discussed above. For example, the +``asyncio.gather()`` function can also receive async functions: + +.. code-block:: python3 + + import asyncio + + async def f(): + await asyncio.sleep(10) + print('f is done') + + async def g(): + await asyncio.sleep(5) + print('g is done') + + async main(): + await asyncio.gather( + asyncio.create_task(f()), + g() + ) + + asyncio.run(main()) + +In this example above, we didn't explicitly use the ``await`` keyword on +the async function ``g()``, but nevertheless it will still be executed. +Inside the ``gather()`` function, the coroutine object returned by ``g()`` +will be wrapped in a ``Task`` object, similar to what we're doing with +``f()``. The ``await gather()`` line above will only return once *both* +``f()`` and ``g()`` have completed (and in fact, it wasn't necessary to +wrap ``f()`` in a task at all here, but it was included just to show that +it works). From 69190b8462867350426c701859caf2befa2a8ddb Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sat, 15 Jun 2019 17:18:33 +1000 Subject: [PATCH 17/31] Fix spurious backtick --- Doc/library/asyncio-tutorial/what-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/asyncio-tutorial/what-asyncio.rst b/Doc/library/asyncio-tutorial/what-asyncio.rst index c59050a203428a..7ce72590d7c724 100644 --- a/Doc/library/asyncio-tutorial/what-asyncio.rst +++ b/Doc/library/asyncio-tutorial/what-asyncio.rst @@ -18,7 +18,7 @@ Let's make a function that communicates over the network: This function will: #. make a socket connection to a host, -#. send ``b'Hello, world```, and +#. send ``b'Hello, world'``, and #. **wait** for a reply. The key point here is about the word *wait*: in the code, execution proceeds line-by-line From 89f7ca26333fb80053ebf98f6ce8d8f542520be1 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sat, 15 Jun 2019 17:34:24 +1000 Subject: [PATCH 18/31] Make the case study (server) a little neater. --- .../asyncio-tutorial/case-study-chat-server.rst | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Doc/library/asyncio-tutorial/case-study-chat-server.rst b/Doc/library/asyncio-tutorial/case-study-chat-server.rst index 5d8650eb1592f3..4e7ca1a3d4da87 100644 --- a/Doc/library/asyncio-tutorial/case-study-chat-server.rst +++ b/Doc/library/asyncio-tutorial/case-study-chat-server.rst @@ -223,8 +223,7 @@ loop. Let's add a bit more error handling: yield json.loads(message) except json.decoder.JSONDecodeError: continue - except (IncompleteReadError, ConnectionAbortedError, - ConnectionResetError, CancelledError): + except (OSError, IncompleteReadError, CancelledError): # The connection is dead, leave. return @@ -241,6 +240,16 @@ deserialise properly. It seems unlikely that a client would send through only a few invalid JSON messages, but the rest valid. For simplicity, we'll keep what we have for now, and move onto +.. note:: + There are several different kinds of connection-related errors, + like ``ConnectionError``, and ``ConnectionAbortedError`` and so on. + Unless you specifically want to know which kind of exception + occurred, it is safe to use ``OSError`` because all the connection-related + exceptions are subclasses of the built-in ``OSError``. The other + exception type to keep an eye on is ``IncompleteReadError`` which is + provided by the ``asyncio`` module, and is *not* a subclass of + ``OSError``. + Sending A Message ^^^^^^^^^^^^^^^^^ @@ -281,7 +290,7 @@ and ``send_message()``, into their own module called ``utils.py`` (Since we'll be using these function in both our server code and our client code!). -For completeness, here is the utils module: +For completeness, here is that final utils module: .. literalinclude:: utils01.py :caption: utils.py From 36fc743ac708cb93cf0999b6a3d0128a6f21dd18 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 16 Jun 2019 01:18:25 +1000 Subject: [PATCH 19/31] Some refactoring and finishing off the server. --- .../case-study-chat-client-cli.rst | 4 +- .../case-study-chat-server.rst | 76 ++++++++++++++----- Doc/library/asyncio-tutorial/server03.py | 5 +- Doc/library/asyncio-tutorial/server04.py | 65 ++++++++++++++++ Doc/library/asyncio-tutorial/server05.py | 73 ++++++++++++++++++ Doc/library/asyncio-tutorial/server20.py | 2 +- Doc/library/asyncio-tutorial/utils01.py | 7 +- Doc/library/asyncio-tutorial/utils20.py | 11 +-- 8 files changed, 208 insertions(+), 35 deletions(-) create mode 100644 Doc/library/asyncio-tutorial/server04.py create mode 100644 Doc/library/asyncio-tutorial/server05.py diff --git a/Doc/library/asyncio-tutorial/case-study-chat-client-cli.rst b/Doc/library/asyncio-tutorial/case-study-chat-client-cli.rst index 482bd24b3500d4..0d7b1f2d6117f0 100644 --- a/Doc/library/asyncio-tutorial/case-study-chat-client-cli.rst +++ b/Doc/library/asyncio-tutorial/case-study-chat-client-cli.rst @@ -1,5 +1,5 @@ -Asyncio Case Study: Chat Application -==================================== +Asyncio Case Study: Chat Application (Client) +============================================= TODO diff --git a/Doc/library/asyncio-tutorial/case-study-chat-server.rst b/Doc/library/asyncio-tutorial/case-study-chat-server.rst index 4e7ca1a3d4da87..c13abd2c762ec7 100644 --- a/Doc/library/asyncio-tutorial/case-study-chat-server.rst +++ b/Doc/library/asyncio-tutorial/case-study-chat-server.rst @@ -141,14 +141,14 @@ to all clients in a room. That message might look something like this: The message is similar to the one received by a client, but on line 5 we now need to indicate from whom the message was sent. -Message-based Protocol ----------------------- +Message Structure +----------------- Now we have a rough idea about what messages between client and -server will look like. Unfortunately, the Streams API, like the +server will look like. The Streams API, like the underlying TCP protocol it wraps, does not give us message-handling built-in. All we get is a stream of bytes. It is up to us to -know how to break up the stream of bytes into recognizable messages. +decide how to break up the stream of bytes into recognizable messages. The most common raw message structure is based on the idea of a *size prefix*: @@ -267,9 +267,7 @@ very similar to what we saw in the receiver: async def send_message(writer: StreamWriter, message: Dict): payload = json.dumps(message).encode() size_prefix = len(payload).to_bytes(3, byteorder='little') - writer.write(size_prefix) - writer.write(payload) - await writer.drain() + await writer.writelines([size_prefix, payload]) Let's step through the lines: @@ -278,12 +276,10 @@ Let's step through the lines: - line 6: Serialize the message to bytes. - line 7: Build the size header; remember, this needs to be sent before the payload itself. -- line 8: Write the header to the stream -- line 9: Write the payload to the stream. Note that because there - is no ``await`` keyword between sending the header and the payload, - we can be sure that there will be no "context switch" between - different async function calls trying to write data to this stream. -- line 10: Finally, wait for all the bytes to be sent. +- line 8: Write both the size header and the payload to the stream; we + could have concatenated the bytes and simply used ``await writer.write()``, + that would make a full copy of the bytes in ``payload``, and for large + messages those extra copies will add up very quickly! We can place the two async functions above, ``new_messages()`` and ``send_message()``, into their own module called ``utils.py`` @@ -335,9 +331,55 @@ able to see print output for each different kind of action received. The next thing we'll have to do is set up chat rooms. There's no point receiving messages if there's nowhere to put them! -TODO +Server: Room Handling +--------------------- -Notes: +We need collections to store which connections are active, and which +rooms each user has joined. We'll manage these events inside the callback +function ``client_connected_cb()``. Here's a snippet of just that, and +the global collections we'll use to track connections and room +membership: -- then show the server -- spend some time on clean shutdown. +.. code-block:: python3 + :caption: Joining and leaving rooms + :linenos: + + from collections import defaultdict + from weakref import WeakValueDictionary + + WRITERS: Dict[str, StreamWriter] = WeakValueDictionary() + ROOMS: Dict[str, WeakSet[StreamWriter]] = defaultdict(WeakSet) + + async def client_connected(reader: StreamReader, writer: StreamWriter): + addr = writer.get_extra_info('peername') + WRITERS[addr] = writer + + def connect(msg): + print(f"User connected: {msg.get('username')}") + + def joinroom(msg): + room_name = msg["room"] + print('joining room:', room_name) + room = ROOMS[room_name] + room.add(writer) + + def leaveroom(msg): + room_name = msg["room"] + print('leaving room:', msg.get('room')) + room = ROOMS[room_name] + room.discard(writer) + + def chat(msg): + print(f'chat sent to room {msg.get("room")}: {msg.get("message")}') + # TODO: distribute the message + + + +- Using ``WeakValueDictionary`` and ``WeakSet`` means that when the + writer object goes out of scope, any entries in the collection of + writers and rooms will automatically be cleaned up. + +The room management is quite simple. When we receive a request to join +a room is received, that room is looked up by name (or created automatically +by the ``defaultdict``) and that connection is added to that room. The +inverse happens when a request is received to leave a room. diff --git a/Doc/library/asyncio-tutorial/server03.py b/Doc/library/asyncio-tutorial/server03.py index 35fa29dab182fc..e562668ffaa829 100644 --- a/Doc/library/asyncio-tutorial/server03.py +++ b/Doc/library/asyncio-tutorial/server03.py @@ -20,10 +20,10 @@ def connect(msg): print(msg.get('username')) def joinroom(msg): - print('joining room:', msg.get('username')) + print('joining room:', msg.get('room')) def leaveroom(msg): - print('leaving room:', msg.get('username')) + print('leaving room:', msg.get('room')) def chat(msg): print(f'chat sent to room {msg.get("room")}: {msg.get("message")}') @@ -49,4 +49,3 @@ def chat(msg): if __name__ == '__main__': asyncio.run(main()) - diff --git a/Doc/library/asyncio-tutorial/server04.py b/Doc/library/asyncio-tutorial/server04.py new file mode 100644 index 00000000000000..611ce55cdf572d --- /dev/null +++ b/Doc/library/asyncio-tutorial/server04.py @@ -0,0 +1,65 @@ +import asyncio +from asyncio import StreamReader, StreamWriter +from collections import defaultdict +from weakref import WeakValueDictionary, WeakSet +from typing import Dict, Callable, Set +from utils import new_messages + +WRITERS: Dict[str, StreamWriter] = WeakValueDictionary() +ROOMS: Dict[str, Set[StreamWriter]] = defaultdict(WeakSet) + + +async def main(): + server = await asyncio.start_server( + client_connected_cb=client_connected, + host='localhost', + port='9011', + ) + async with server: + await server.serve_forever() + + +async def client_connected(reader: StreamReader, writer: StreamWriter): + addr = writer.get_extra_info('peername') + WRITERS[addr] = writer + + def connect(msg): + print(f"User connected: {msg.get('username')}") + + def joinroom(msg): + room_name = msg["room"] + print('joining room:', room_name) + room = ROOMS[room_name] + room.add(writer) + + def leaveroom(msg): + room_name = msg["room"] + print('leaving room:', msg.get('room')) + room = ROOMS[room_name] + room.discard(writer) + + def chat(msg): + print(f'chat sent to room {msg.get("room")}: {msg.get("message")}') + # TODO: distribute the message + + handlers: Dict[str, Callable] = dict( + connect=connect, + joinroom=joinroom, + leaveroom=leaveroom, + chat=chat, + ) + + async for msg in new_messages(reader): + action = msg.get('action') + if not action: + continue + + handler = handlers.get(action) + if not handler: + continue + + handler(msg) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/Doc/library/asyncio-tutorial/server05.py b/Doc/library/asyncio-tutorial/server05.py new file mode 100644 index 00000000000000..9ef1ea932c3529 --- /dev/null +++ b/Doc/library/asyncio-tutorial/server05.py @@ -0,0 +1,73 @@ +import asyncio +from asyncio import StreamReader, StreamWriter +from collections import defaultdict +from weakref import WeakValueDictionary, WeakSet +from typing import Dict, Callable, Set +import utils +from utils import new_messages +import json + +ROOMS: Dict[str, Set[StreamWriter]] = defaultdict(WeakSet) + + +async def main(): + server = await asyncio.start_server( + client_connected_cb=client_connected, + host='localhost', + port='9011', + ) + async with server: + await server.serve_forever() + + +async def client_connected(reader: StreamReader, writer: StreamWriter): + def connect(msg): + print(f"User connected: {msg.get('username')}") + + def joinroom(msg): + room_name = msg["room"] + print('joining room:', room_name) + room = ROOMS[room_name] + room.add(writer) + + def leaveroom(msg): + room_name = msg["room"] + print('leaving room:', msg.get('room')) + room = ROOMS[room_name] + room.discard(writer) + + def chat(msg): + print(f'chat sent to room {msg.get("room")}: {msg.get("message")}') + for denizen in ROOMS[msg["room"]]: + print(f'Sending message: {msg}') + asyncio.create_task(sender(denizen, msg)) + + handlers: Dict[str, Callable] = dict( + connect=connect, + joinroom=joinroom, + leaveroom=leaveroom, + chat=chat, + ) + + async for msg in new_messages(reader): + action = msg.get('action') + if not action: + continue + + handler = handlers.get(action) + if not handler: + continue + + handler(msg) + + +async def sender(writer: StreamWriter, msg: Dict): + try: + await utils.send_message(writer, json.dumps(msg).encode()) + except OSError: + """ Connection is dead, remove it.""" + ROOMS[msg["room"]].discard(writer) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/Doc/library/asyncio-tutorial/server20.py b/Doc/library/asyncio-tutorial/server20.py index 65d4dd457c83b5..1385615bf0b04a 100644 --- a/Doc/library/asyncio-tutorial/server20.py +++ b/Doc/library/asyncio-tutorial/server20.py @@ -16,7 +16,7 @@ async def sender(addr, writer, room, msg): writer, json.dumps(dict(room=room, msg=msg)).encode() ) - except (ConnectionAbortedError, ConnectionResetError): + except OSError: """ Connection is dead, remove it.""" if addr in WRITERS: del WRITERS[addr] diff --git a/Doc/library/asyncio-tutorial/utils01.py b/Doc/library/asyncio-tutorial/utils01.py index 8ab7f0d5d8211a..e6a4b4eebf3af4 100644 --- a/Doc/library/asyncio-tutorial/utils01.py +++ b/Doc/library/asyncio-tutorial/utils01.py @@ -15,8 +15,7 @@ async def new_messages(reader: StreamReader) -> AsyncGenerator[Dict, None]: yield json.loads(message) except json.decoder.JSONDecodeError: continue - except (IncompleteReadError, ConnectionAbortedError, - ConnectionResetError, CancelledError): + except (OSError, IncompleteReadError, CancelledError): # The connection is dead, leave. return @@ -24,6 +23,4 @@ async def new_messages(reader: StreamReader) -> AsyncGenerator[Dict, None]: async def send_message(writer: StreamWriter, message: Dict): payload = json.dumps(message).encode() size_prefix = len(payload).to_bytes(3, byteorder='little') - writer.write(size_prefix) - writer.write(payload) - await writer.drain() + await writer.writelines([size_prefix, payload]) diff --git a/Doc/library/asyncio-tutorial/utils20.py b/Doc/library/asyncio-tutorial/utils20.py index 883d14a2681ef3..7c8f708a0a26b5 100644 --- a/Doc/library/asyncio-tutorial/utils20.py +++ b/Doc/library/asyncio-tutorial/utils20.py @@ -1,6 +1,6 @@ import sys from asyncio import (StreamReader, StreamWriter, IncompleteReadError, Future, - get_running_loop) + get_running_loop, CancelledError) if sys.platform == 'win32': from signal import signal, SIGBREAK, SIGTERM, SIGINT @@ -19,20 +19,17 @@ async def messages(reader: StreamReader) -> AsyncGenerator[bytes, None]: size = int.from_bytes(size_prefix, byteorder='little') message = await reader.readexactly(size) yield message - except (IncompleteReadError, ConnectionAbortedError, ConnectionResetError): + except (OSError, IncompleteReadError, CancelledError): return async def send_message(writer: StreamWriter, message: bytes): """To close the connection, use an empty message.""" if not message: - writer.close() - await writer.wait_closed() + await writer.close() return size_prefix = len(message).to_bytes(4, byteorder='little') - writer.write(size_prefix) - writer.write(message) - await writer.drain() + await writer.writelines([size_prefix, message]) def install_signal_handling(fut: Future): From d55d8fbabf01813a61571ac353323dc9c857f153 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 16 Jun 2019 18:34:19 +1000 Subject: [PATCH 20/31] Cleaned up the last bit of the chat server code sample. --- .../case-study-chat-server.rst | 65 ++++++++----------- Doc/library/asyncio-tutorial/server05.py | 36 +++------- 2 files changed, 35 insertions(+), 66 deletions(-) diff --git a/Doc/library/asyncio-tutorial/case-study-chat-server.rst b/Doc/library/asyncio-tutorial/case-study-chat-server.rst index c13abd2c762ec7..981eeb0c67f58d 100644 --- a/Doc/library/asyncio-tutorial/case-study-chat-server.rst +++ b/Doc/library/asyncio-tutorial/case-study-chat-server.rst @@ -340,46 +340,33 @@ function ``client_connected_cb()``. Here's a snippet of just that, and the global collections we'll use to track connections and room membership: -.. code-block:: python3 +.. literalinclude:: server05.py :caption: Joining and leaving rooms - :linenos: - - from collections import defaultdict - from weakref import WeakValueDictionary - - WRITERS: Dict[str, StreamWriter] = WeakValueDictionary() - ROOMS: Dict[str, WeakSet[StreamWriter]] = defaultdict(WeakSet) - - async def client_connected(reader: StreamReader, writer: StreamWriter): - addr = writer.get_extra_info('peername') - WRITERS[addr] = writer - - def connect(msg): - print(f"User connected: {msg.get('username')}") - - def joinroom(msg): - room_name = msg["room"] - print('joining room:', room_name) - room = ROOMS[room_name] - room.add(writer) - - def leaveroom(msg): - room_name = msg["room"] - print('leaving room:', msg.get('room')) - room = ROOMS[room_name] - room.discard(writer) - - def chat(msg): - print(f'chat sent to room {msg.get("room")}: {msg.get("message")}') - # TODO: distribute the message - - - -- Using ``WeakValueDictionary`` and ``WeakSet`` means that when the - writer object goes out of scope, any entries in the collection of - writers and rooms will automatically be cleaned up. + :lines: 9-10,18-40 + :language: python3 -The room management is quite simple. When we receive a request to join +When we receive a request to join a room is received, that room is looked up by name (or created automatically by the ``defaultdict``) and that connection is added to that room. The -inverse happens when a request is received to leave a room. +inverse happens when a request is received to leave a room. When a new chat +message is received, it must be sent to all the other connections in that +room. The ``send_message()`` function is from our ``utils`` module shown +earlier. + +Note how we don't call ``await send_message()``, but instead we create +a separate task for sending to each connection. This is because we don't +want to delay any of these messages. If we used the ``await`` keyword, +then each send call would hold up the next one in the iteration. Using +``create_task()`` in this way allows us to run a coroutine +"in the background", without waiting for it to return before proceeding +with the next lines of code. + +Server: Final Version +--------------------- + +At this point the server part of our project is complete and we can show +the final server module: + +.. literalinclude:: server05.py + :caption: server.py + :language: python3 diff --git a/Doc/library/asyncio-tutorial/server05.py b/Doc/library/asyncio-tutorial/server05.py index 9ef1ea932c3529..caead05fb70a0c 100644 --- a/Doc/library/asyncio-tutorial/server05.py +++ b/Doc/library/asyncio-tutorial/server05.py @@ -2,20 +2,15 @@ from asyncio import StreamReader, StreamWriter from collections import defaultdict from weakref import WeakValueDictionary, WeakSet -from typing import Dict, Callable, Set -import utils -from utils import new_messages +from typing import Dict, Callable, Set, MutableMapping, DefaultDict +from utils import new_messages, send_message import json -ROOMS: Dict[str, Set[StreamWriter]] = defaultdict(WeakSet) +ROOMS: DefaultDict[str, Set[StreamWriter]] = defaultdict(WeakSet) async def main(): - server = await asyncio.start_server( - client_connected_cb=client_connected, - host='localhost', - port='9011', - ) + server = await asyncio.start_server(client_connected, 'localhost', '9011') async with server: await server.serve_forever() @@ -38,11 +33,12 @@ def leaveroom(msg): def chat(msg): print(f'chat sent to room {msg.get("room")}: {msg.get("message")}') - for denizen in ROOMS[msg["room"]]: - print(f'Sending message: {msg}') - asyncio.create_task(sender(denizen, msg)) + payload = json.dumps(msg).encode() + room = ROOMS[msg["room"]] + for friend in room: + asyncio.create_task(send_message(friend, payload)) - handlers: Dict[str, Callable] = dict( + handlers: Dict[str, Callable[[Dict], None]] = dict( connect=connect, joinroom=joinroom, leaveroom=leaveroom, @@ -51,23 +47,9 @@ def chat(msg): async for msg in new_messages(reader): action = msg.get('action') - if not action: - continue - handler = handlers.get(action) - if not handler: - continue - handler(msg) -async def sender(writer: StreamWriter, msg: Dict): - try: - await utils.send_message(writer, json.dumps(msg).encode()) - except OSError: - """ Connection is dead, remove it.""" - ROOMS[msg["room"]].discard(writer) - - if __name__ == '__main__': asyncio.run(main()) From 34306f0ae8e78f0e0360775b8b00d054962c8303 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 16 Jun 2019 23:37:18 +1000 Subject: [PATCH 21/31] Further progress - got a CLI chat client working using prompt-toolkit. --- .../case-study-chat-server.rst | 2 +- Doc/library/asyncio-tutorial/client05.py | 36 +++++++++++++++++++ Doc/library/asyncio-tutorial/pttest.py | 32 +++++++++++++++++ Doc/library/asyncio-tutorial/server05.py | 3 +- 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 Doc/library/asyncio-tutorial/client05.py create mode 100644 Doc/library/asyncio-tutorial/pttest.py diff --git a/Doc/library/asyncio-tutorial/case-study-chat-server.rst b/Doc/library/asyncio-tutorial/case-study-chat-server.rst index 981eeb0c67f58d..38f4155106b8f9 100644 --- a/Doc/library/asyncio-tutorial/case-study-chat-server.rst +++ b/Doc/library/asyncio-tutorial/case-study-chat-server.rst @@ -342,7 +342,7 @@ membership: .. literalinclude:: server05.py :caption: Joining and leaving rooms - :lines: 9-10,18-40 + :lines: 9-10,18-39 :language: python3 When we receive a request to join diff --git a/Doc/library/asyncio-tutorial/client05.py b/Doc/library/asyncio-tutorial/client05.py new file mode 100644 index 00000000000000..a89c2cb589df3a --- /dev/null +++ b/Doc/library/asyncio-tutorial/client05.py @@ -0,0 +1,36 @@ +import asyncio +from utils import new_messages, send_message + +from prompt_toolkit.eventloop.defaults import use_asyncio_event_loop +from prompt_toolkit.patch_stdout import patch_stdout +from prompt_toolkit.shortcuts import PromptSession + + +async def main(): + use_asyncio_event_loop() + + reader, writer = await asyncio.open_connection('localhost', '9011') + await send_message(writer, dict(action='connect', username='Eric')) + await send_message(writer, dict(action='joinroom', room='nonsense')) + + asyncio.create_task(enter_message(writer)) + + async for msg in new_messages(reader): + print(f"{msg['from']}: {msg['message']}") + + +async def enter_message(writer): + session = PromptSession('Send message: ', erase_when_done=True) + while True: + try: + msg = await session.prompt(async_=True) + if not msg: + continue + await send_message(writer, msg) + except (EOFError, asyncio.CancelledError): + return + + +if __name__ == '__main__': + with patch_stdout(): + asyncio.run(main()) diff --git a/Doc/library/asyncio-tutorial/pttest.py b/Doc/library/asyncio-tutorial/pttest.py new file mode 100644 index 00000000000000..b7fb5e82073ef8 --- /dev/null +++ b/Doc/library/asyncio-tutorial/pttest.py @@ -0,0 +1,32 @@ +import asyncio + +from prompt_toolkit.eventloop.defaults import use_asyncio_event_loop +from prompt_toolkit.patch_stdout import patch_stdout +from prompt_toolkit.shortcuts import PromptSession + + +async def blah(): + while True: + print('.') + await asyncio.sleep(10.0) + + +async def prompt(): + session = PromptSession('Message: ', erase_when_done=True) + while True: + try: + msg = await session.prompt(async_=True) + print(msg) + except (EOFError, asyncio.CancelledError): + return + + +async def main(): + use_asyncio_event_loop() + await asyncio.gather(blah(), prompt()) + # await asyncio.gather(blah()) + + +if __name__ == '__main__': + with patch_stdout(): + asyncio.run(main()) diff --git a/Doc/library/asyncio-tutorial/server05.py b/Doc/library/asyncio-tutorial/server05.py index caead05fb70a0c..ca542f5ef0ce93 100644 --- a/Doc/library/asyncio-tutorial/server05.py +++ b/Doc/library/asyncio-tutorial/server05.py @@ -33,10 +33,9 @@ def leaveroom(msg): def chat(msg): print(f'chat sent to room {msg.get("room")}: {msg.get("message")}') - payload = json.dumps(msg).encode() room = ROOMS[msg["room"]] for friend in room: - asyncio.create_task(send_message(friend, payload)) + asyncio.create_task(send_message(friend, msg)) handlers: Dict[str, Callable[[Dict], None]] = dict( connect=connect, From 0c827556c1f7451be8c8e4826a14a51de9d12f24 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Sun, 16 Jun 2019 23:51:44 +1000 Subject: [PATCH 22/31] Include chat client code in the text. --- .../asyncio-tutorial/case-study-chat-client-cli.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Doc/library/asyncio-tutorial/case-study-chat-client-cli.rst b/Doc/library/asyncio-tutorial/case-study-chat-client-cli.rst index 0d7b1f2d6117f0..37c1ae9ef6eb0c 100644 --- a/Doc/library/asyncio-tutorial/case-study-chat-client-cli.rst +++ b/Doc/library/asyncio-tutorial/case-study-chat-client-cli.rst @@ -1,6 +1,13 @@ Asyncio Case Study: Chat Application (Client) ============================================= +WIP + +.. literalinclude:: client05.py + :caption: client.py + :language: python3 + + TODO Notes: From a774a9895de23825a056313d72b23e54272c84a6 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Mon, 17 Jun 2019 21:04:25 +1000 Subject: [PATCH 23/31] Fix typo --- Doc/library/asyncio-tutorial/what-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/asyncio-tutorial/what-asyncio.rst b/Doc/library/asyncio-tutorial/what-asyncio.rst index 7ce72590d7c724..806cb998879c59 100644 --- a/Doc/library/asyncio-tutorial/what-asyncio.rst +++ b/Doc/library/asyncio-tutorial/what-asyncio.rst @@ -145,7 +145,7 @@ There are a couple of new things here, but I want you to focus on the new keyword ``await``. Unlike threads, execution is allowed to switch between the two ``greet()`` invocations **only** where the ``await`` keyword appears. On all other lines, execution is exactly the -same as normal Python, and will not be preempt by thread switching (there's +same as normal Python, and will not be preempted by thread switching (there's typically only a single thread in most ``asyncio`` programs). These ``async def`` functions are called "asynchronous" because execution does not pass through the function From eedbc9783912fcf35a122e500b25047e179db272 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Mon, 17 Jun 2019 21:07:56 +1000 Subject: [PATCH 24/31] Clarify switching behaviour --- Doc/library/asyncio-tutorial/what-asyncio.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/asyncio-tutorial/what-asyncio.rst b/Doc/library/asyncio-tutorial/what-asyncio.rst index 806cb998879c59..e3d21c638f05ae 100644 --- a/Doc/library/asyncio-tutorial/what-asyncio.rst +++ b/Doc/library/asyncio-tutorial/what-asyncio.rst @@ -143,7 +143,7 @@ run concurrently. There are a couple of new things here, but I want you to focus on the new keyword ``await``. Unlike threads, execution is allowed to -switch between the two ``greet()`` invocations **only** where the +switch between concurrent tasks **only** at places where the ``await`` keyword appears. On all other lines, execution is exactly the same as normal Python, and will not be preempted by thread switching (there's typically only a single thread in most ``asyncio`` programs). @@ -151,11 +151,11 @@ These ``async def`` functions are called "asynchronous" because execution does not pass through the function top-down, but instead can suspend in the middle of a function at the ``await`` keyword, and allow another function to execute while -*this function* is waiting for network data. +*this function* is waiting for I/O (usually network) data. An additional advantage of the *async* style above is that it lets us -manage several thousand concurrent long-lived socket connections in a simple way. -One can also use threads to manage concurrent long-lived socket connections, +manage many thousands of concurrent, long-lived socket connections in a simple way. +One *can* also use threads to manage concurrent long-lived socket connections, but it gets difficult to go past a few thousand because the creation of operating system threads, just like processes, consumes additional resources from the operating system. From a8a801d95ba9b2c07488e3b82f24958cc786c526 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Mon, 17 Jun 2019 23:27:51 +1000 Subject: [PATCH 25/31] Add async generators and async context managers discussion. --- .../asyncio-tutorial/async-functions.rst | 146 +++++++++++++++++- 1 file changed, 139 insertions(+), 7 deletions(-) diff --git a/Doc/library/asyncio-tutorial/async-functions.rst b/Doc/library/asyncio-tutorial/async-functions.rst index 9008c586f13f4f..ce76f3d876cac9 100644 --- a/Doc/library/asyncio-tutorial/async-functions.rst +++ b/Doc/library/asyncio-tutorial/async-functions.rst @@ -1,5 +1,5 @@ -Functions: Sync vs Async -======================== +Async Functions, And Other Syntax Features +========================================== Regular Python functions are created with the keyword ``def``, and look like this: @@ -160,6 +160,13 @@ a dramatic simplifying effect on your code, because now you can modify data shared between multiple async tasks without fear of introducing a race condition. +.. note:: In programs using ``asyncio``, you should never use ``time.sleep()``. + The correct way to "sleep" is with ``await asyncio.sleep()``. This is + because ``time.sleep()`` is a *blocking* call that will prevent the + ``asyncio`` event loop from processing events. The only safe way to + use ``time.sleep()`` is within a thread, or a subprocess, or with a + value of zero! + Accurate Terminology For Async Functions ---------------------------------------- @@ -303,14 +310,18 @@ really is, so let's jump directly to some examples: >>> import asyncio >>> async def ag(): - ... yield 123 + ... yield 1 + ... yield 2 + ... yield 3 ... >>> async def main(): ... async for value in ag(): ... print(value) ... >>> asyncio.run(main()) - 123 + 1 + 2 + 3 If you pretend for a second that the word "async" is temporarily removed from the code above, the behaviour of the generator @@ -321,8 +332,8 @@ generator. The difference now is of course the presence of those "async" words. The code sample doesn't show a good reason *why* an async -generator is being used here: that will come later in the -cookbook. All we want to discuss here is what these kinds of +generator is being used here: that comes a bit further down. +All we want to discuss here is what these kinds of functions and objects should be called. Let's have a close look at the function `ag`: @@ -330,7 +341,7 @@ Let's have a close look at the function `ag`: .. code-block:: python3 >>> async def ag(): - ... yield 123 + ... yield 1 ... >>> inspect.isfunction(ag) True @@ -351,3 +362,124 @@ Let's have a close look at the function `ag`: True # ...and when evaluated, it returns an "async generator" + +Hopefully you're comfortable now with how async generators look. Let's +briefly discuss why you might want to use them. In the examples given +above, there was no good read to make our generator an ``async def`` +function; an ordinary generator function would have been fine. Async +generators are useful when you need to ``await`` on another coroutine +either before, or after, each ``yield``. + +One example might be receiving network data from a ``StreamReader`` +instance: + +.. code-block:: python3 + + async def new_messages(reader: StreamReader): + while True: + data = await reader.read(1024) + yield data + +This pattern makes for a very clean consumer of the received data: + +.. code-block:: python3 + + async def get_data(): + reader, writer = await asyncio.open_connection(...) + async for data in new_messages(reader): + do_something_with(data) + +Async generators allow you to improve your abstractions: for +example, you can go one level higher and handle reconnection +while still propagating received data out to a consumer: + +.. code-block:: python3 + + async def new_messages(reader: StreamReader): + while True: + data = await reader.read(1024) + yield data # (1) + + async def get_data(host, port): + while True: + try: + reader, writer = await asyncio.open_connection(host, port) + async for data in new_messages(reader): + if not data: + continue + yield data # (2) + except OSError: + continue + except asyncio.CancelledError: + return + + async def main(host, port): + async for data in get_data(host, port): + do_something_with(data) # (3) + + if __name__ == '__main__': + asyncio.run(main(host, port)) + +The async generator at ``(1)`` provides results back to an intermediate +async generator at ``(2)``, which does *the same thing* but also handles +reconnection events in its local scope. Finally, at ``(3)``, The async +iterator elegantly produces the received data, and internal reconnection +events (and any other lower level state management) are hidden from the +high-level logic of the application. + +Async Context Managers +---------------------- + +In the previous section we showed how async generators can be driven +with the new ``async for`` syntax. There is also a version of +a *context manager* that can be used with ``asyncio``. + +.. note:: There is a common misconception that one **must** use + async context managers in ``asyncio`` applications. This is not the + case. Async context managers are needed only if you need to ``await`` + a coroutine in the *enter* or *exit* parts of the context manager. + You do *not* required to use an async context manager if there are ``await`` + statements inside only the *body* of the context manager. + +Just as the ``contextlib`` library provides the ``@contextmanager`` +decorator to let us easily make context managers, so does the +``@asynccontextmanager`` let us do that for async context managers. + +Imagine a very simple example where we might want to have a +connection closed during cancellation, and how about adding some +logging around the connection lifecycle events: + +.. code-block:: python3 + + import asyncio + import logging + from contextlib import asynccontextmanager + + @asynccontextmanager + async def open_conn_logged(*args, **kwargs): + logging.info('Opening connection...') + reader, writer = await asyncio.open_connection(*args, **kwargs) + logging.info('Connection opened.') + try: + yield reader, writer # (1) + finally: + logging.info('Cleaning up connection...') + if not writer.is_closing(): + await writer.close() + logging.info('Connection closed.') + + async def echo(): + async with open_conn_logged('localhost', 8000) as (reader, writer): + data = await reader.read(1024) + await writer.write(data) + + if __name__ == '__main__': + asyncio.run(echo()) + +At line marked ``(1)``, data is provided to the context inside the ``echo()`` +function. You can see how the ``async with`` keywords are required to +work with the async context manager. + +Async context managers are likely to appear in projects using +``asyncio`` because the need to safely close or dispose of resources is +very common in network programming. From 8e6dcfdc2ec6826c10f52c40d634a49006d74ada Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Tue, 18 Jun 2019 00:18:40 +1000 Subject: [PATCH 26/31] Add some comparison with JavaScript async/await and asyncio.create_task --- .../running-async-functions.rst | 104 +++++++++++++++++- 1 file changed, 98 insertions(+), 6 deletions(-) diff --git a/Doc/library/asyncio-tutorial/running-async-functions.rst b/Doc/library/asyncio-tutorial/running-async-functions.rst index fea02491181a07..65a4154bc0999d 100644 --- a/Doc/library/asyncio-tutorial/running-async-functions.rst +++ b/Doc/library/asyncio-tutorial/running-async-functions.rst @@ -1,11 +1,11 @@ -Executing Async Functions -========================= +Three Ways To Execute Async Functions +===================================== In a previous section we looked at the difference between sync functions -and async functions. Here we focus specifically on async functions, and -how to call them. +and async functions, and other async language syntax features. +Here we focus specifically on how to execute async functions. -Imagine we have an async function, called ``my_coro_fn``, and we want to +Imagine we have an async function, called ``my_coro_fn()``, and we want to run it. There are three ways: 1. ``asyncio.run(my_coro_fn())`` @@ -53,7 +53,8 @@ passed to ``create_task()``, and immediately after, async function ``g()`` is called with ``await g()``. Even though ``f()`` is called first, async function ``g()`` will finish -first, and you'll see "g is done" printed before "f is done". This is because +first (5 seconds is shorter than 10 seconds), and you'll see "g is done" +printed before "f is done". This is because although ``create_task()`` does schedule the given async function to be executed, it does not wait for the call to complete, unlike when the ``await`` keyword is used. @@ -117,3 +118,94 @@ will be wrapped in a ``Task`` object, similar to what we're doing with ``f()`` and ``g()`` have completed (and in fact, it wasn't necessary to wrap ``f()`` in a task at all here, but it was included just to show that it works). + +.. note:: The ``create_task()`` API is useful to understand concurrency + features in Modern JavaScript, or *vice-versa* if you're coming to + Python from the context of JavaScript. JS also has ``async`` + and ``await`` keywords, and they work *almost* exactly the same as + described in this Python tutorial! There is however one big + difference: In JavaScript, all async functions, when called, behave + like ``asyncio.create_task()`` calls. Consider the following + JavaScript code: + + .. code-block:: javascript + + async func1 () { + return await http.get('http://example.com/1') + } + async func2 () { + return await http.get('http://example.com/2') + } + async main () { + task1 = func1() // In Python: `task1 = create_task(func1())` + task2 = func2() // In Python: `task2 = create_task(func2())` + [result1, result2] = [await task1, await task2] + } + + In Python, when you see two ``await`` keywords in series, it usually + reads as "first the one, then the other". This is because the ``await`` + keyword suspends the calling context until the coroutine returns. + In the JavaScript shown above, that is not the case, both ``task1`` + *and* ``task2`` will run concurrently, although ``result1`` and + ``result2`` will only be set when both tasks have completed. + + A naive translation of the JavaScript code to Python might look + like this: + + .. code-block:: python3 + + async def func1(): + return await http.get('http://example.com/1') + + async func2(): + return await http.get('http://example.com/2') + + async def main(): + coro1 = func1() + coro2 = func2() + [result1, result2] = [await coro1, await coro2] + } + + However, this will *not* behave the same: ``coro2`` will begin + running only after ``coro1`` has completed! Instead, one can use + Python's ``create_task()`` to more closely mimic the JavaScript + behaviour: + + .. code-block:: python3 + + async def func1(): + return await http.get('http://example.com/1') + + async func2(): + return await http.get('http://example.com/2') + + async def main(): + task1 = asyncio.create_task(func1()) + task2 = asyncio.create_task(func2()) + [result1, result2] = [await task1, await task2] + } + + Now ``task1`` and ``task2`` will run concurrently, and the results + will be assigned only after both tasks are complete. Of course, this is + not idiomatic in Python: the more common pattern for waiting on + several coroutines concurrently is with the ``gather`` API, which + includes a highly-recommended error-handling feature: + + .. code-block:: python3 + + async def main(): + [result1, result2] = await asyncio.gather( + func1(), func2(), return_exceptions=True + ) + + Setting ``return_exceptions=True`` makes raised exceptions from + any of the given coroutines become "returned" values instead, and + then it is up to you to check whether either of ``result1`` or + ``result2`` is an ``Exception`` type. + + The documentation for ``asyncio.gather()`` has an important warning: + if ``return_exceptions=False``, any exception raised from one of the + coroutines will bubble up into your calling code. This will cause + the ``gather`` call to terminate, but the *other* coroutines supplied + to the ``gather()`` call will **not** be affected, and will continue + to run. From 0e5ed3f9423d68adb1d1df5a272d4625edf29589 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Tue, 18 Jun 2019 00:31:01 +1000 Subject: [PATCH 27/31] Fix "no good read" typo --- Doc/library/asyncio-tutorial/async-functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/asyncio-tutorial/async-functions.rst b/Doc/library/asyncio-tutorial/async-functions.rst index ce76f3d876cac9..f6126cded2b749 100644 --- a/Doc/library/asyncio-tutorial/async-functions.rst +++ b/Doc/library/asyncio-tutorial/async-functions.rst @@ -365,7 +365,7 @@ Let's have a close look at the function `ag`: Hopefully you're comfortable now with how async generators look. Let's briefly discuss why you might want to use them. In the examples given -above, there was no good read to make our generator an ``async def`` +above, there was no good reason to make our generator an ``async def`` function; an ordinary generator function would have been fine. Async generators are useful when you need to ``await`` on another coroutine either before, or after, each ``yield``. From 4714ed2c10fb805667edd4fc63cf1b13c0fa0a58 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Tue, 18 Jun 2019 00:34:25 +1000 Subject: [PATCH 28/31] Fix "do not required" typo --- Doc/library/asyncio-tutorial/async-functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/asyncio-tutorial/async-functions.rst b/Doc/library/asyncio-tutorial/async-functions.rst index f6126cded2b749..259de714e7174e 100644 --- a/Doc/library/asyncio-tutorial/async-functions.rst +++ b/Doc/library/asyncio-tutorial/async-functions.rst @@ -438,7 +438,7 @@ a *context manager* that can be used with ``asyncio``. async context managers in ``asyncio`` applications. This is not the case. Async context managers are needed only if you need to ``await`` a coroutine in the *enter* or *exit* parts of the context manager. - You do *not* required to use an async context manager if there are ``await`` + You are *not* required to use an async context manager if there are ``await`` statements inside only the *body* of the context manager. Just as the ``contextlib`` library provides the ``@contextmanager`` From d71da6709d7878ea696ec1480640e8b7dd1a2b44 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Tue, 18 Jun 2019 00:39:41 +1000 Subject: [PATCH 29/31] Modern -> modern --- Doc/library/asyncio-tutorial/running-async-functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/asyncio-tutorial/running-async-functions.rst b/Doc/library/asyncio-tutorial/running-async-functions.rst index 65a4154bc0999d..aa84b93cb50604 100644 --- a/Doc/library/asyncio-tutorial/running-async-functions.rst +++ b/Doc/library/asyncio-tutorial/running-async-functions.rst @@ -120,7 +120,7 @@ wrap ``f()`` in a task at all here, but it was included just to show that it works). .. note:: The ``create_task()`` API is useful to understand concurrency - features in Modern JavaScript, or *vice-versa* if you're coming to + features in modern JavaScript, or *vice-versa* if you're coming to Python from the context of JavaScript. JS also has ``async`` and ``await`` keywords, and they work *almost* exactly the same as described in this Python tutorial! There is however one big From 26cc634f663179710577b3ca1cb7b9d429b984f6 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Wed, 19 Jun 2019 22:35:45 +1000 Subject: [PATCH 30/31] Removing the GUI case study section --- .../case-study-chat-client-gui.rst | 20 ------------------- Doc/library/asyncio-tutorial/index.rst | 1 - 2 files changed, 21 deletions(-) delete mode 100644 Doc/library/asyncio-tutorial/case-study-chat-client-gui.rst diff --git a/Doc/library/asyncio-tutorial/case-study-chat-client-gui.rst b/Doc/library/asyncio-tutorial/case-study-chat-client-gui.rst deleted file mode 100644 index 0f693d7613bcc9..00000000000000 --- a/Doc/library/asyncio-tutorial/case-study-chat-client-gui.rst +++ /dev/null @@ -1,20 +0,0 @@ -Asyncio Case Study: Chat Application with GUI client -==================================================== - -TODO - -Notes: - -- server code remains identical to prior case study -- focus is on making a nice client -- The focus area here is: can you use asyncio if there is another - blocking "loop" in the main thread? (common with GUIs and games) - How do you do that? -- Mention any special considerations -- Show and discuss strategies for passing data between main thread - (GUI) and the asyncio thread (IO). -- We can demonstrate the above with tkinter, allowing the - case study to depend only on the stdlib -- Towards the end, mention how the design might change if - the client was a browser instead of a desktop client. - (can refer to the 3rd party websocket library, or aiohttp) diff --git a/Doc/library/asyncio-tutorial/index.rst b/Doc/library/asyncio-tutorial/index.rst index 30671bb5a449da..202f2d0294f3dd 100644 --- a/Doc/library/asyncio-tutorial/index.rst +++ b/Doc/library/asyncio-tutorial/index.rst @@ -22,4 +22,3 @@ primarily on the "high-level" API, as described in the asyncio-cookbook.rst case-study-chat-server.rst case-study-chat-client-cli.rst - case-study-chat-client-gui.rst From 953002165123a6451cebaeae9e0841b9274bd6f8 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Wed, 11 Sep 2019 22:59:15 +1000 Subject: [PATCH 31/31] Remove problematic backticks inside a code-block --- Doc/library/asyncio-tutorial/running-async-functions.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/asyncio-tutorial/running-async-functions.rst b/Doc/library/asyncio-tutorial/running-async-functions.rst index aa84b93cb50604..9937d9bf3ffa1a 100644 --- a/Doc/library/asyncio-tutorial/running-async-functions.rst +++ b/Doc/library/asyncio-tutorial/running-async-functions.rst @@ -137,8 +137,8 @@ it works). return await http.get('http://example.com/2') } async main () { - task1 = func1() // In Python: `task1 = create_task(func1())` - task2 = func2() // In Python: `task2 = create_task(func2())` + task1 = func1() // In Python: task1 = create_task(func1()) + task2 = func2() // In Python: task2 = create_task(func2()) [result1, result2] = [await task1, await task2] }