mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-08 04:52:30 +00:00
Completely overhaul the documentation
This commit is contained in:
362
readthedocs/concepts/asyncio.rst
Normal file
362
readthedocs/concepts/asyncio.rst
Normal file
@@ -0,0 +1,362 @@
|
||||
.. _mastering-asyncio:
|
||||
|
||||
=================
|
||||
Mastering asyncio
|
||||
=================
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
What's asyncio?
|
||||
===============
|
||||
|
||||
asyncio_ is a Python 3's built-in library. This means it's already installed if
|
||||
you have Python 3. Since Python 3.5, it is convenient to work with asynchronous
|
||||
code. Before (Python 3.4) we didn't have ``async`` or ``await``, but now we do.
|
||||
|
||||
asyncio_ stands for *Asynchronous Input Output*. This is a very powerful
|
||||
concept to use whenever you work IO. Interacting with the web or external
|
||||
APIs such as Telegram's makes a lot of sense this way.
|
||||
|
||||
|
||||
Why asyncio?
|
||||
============
|
||||
|
||||
Asynchronous IO makes a lot of sense in a library like Telethon.
|
||||
You send a request to the server (such as "get some message"), and
|
||||
thanks to asyncio_, your code won't block while a response arrives.
|
||||
|
||||
The alternative would be to spawn a thread for each update so that
|
||||
other code can run while the response arrives. That is *a lot* more
|
||||
expensive.
|
||||
|
||||
The code will also run faster, because instead of switching back and
|
||||
forth between the OS and your script, your script can handle it all.
|
||||
Avoiding switching saves quite a bit of time, in Python or any other
|
||||
language that supports asynchronous IO. It will also be cheaper,
|
||||
because tasks are smaller than threads, which are smaller than processes.
|
||||
|
||||
|
||||
What are asyncio basics?
|
||||
========================
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# First we need the asyncio library
|
||||
import asyncio
|
||||
|
||||
# Then we need a loop to work with
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# We also need something to run
|
||||
async def main():
|
||||
for char in 'Hello, world!\n':
|
||||
print(char, end='', flush=True)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
# Then, we need to run the loop with a task
|
||||
loop.run_until_complete(main())
|
||||
|
||||
|
||||
What does telethon.sync do?
|
||||
===========================
|
||||
|
||||
The moment you import any of these:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import sync, ...
|
||||
# or
|
||||
from telethon.sync import ...
|
||||
# or
|
||||
import telethon.sync
|
||||
|
||||
The ``sync`` module rewrites most ``async def``
|
||||
methods in Telethon to something similar to this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def new_method():
|
||||
result = original_method()
|
||||
if loop.is_running():
|
||||
# the loop is already running, return the await-able to the user
|
||||
return result
|
||||
else:
|
||||
# the loop is not running yet, so we can run it for the user
|
||||
return loop.run_until_complete(result)
|
||||
|
||||
|
||||
That means you can do this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(client.get_me().username)
|
||||
|
||||
Instead of this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
loop = asyncio.get_event_loop()
|
||||
me = loop.run_until_complete(client.get_me())
|
||||
print(me.username)
|
||||
|
||||
|
||||
As you can see, it's a lot of boilerplate and noise having to type
|
||||
``run_until_complete`` all the time, so you can let the magic module
|
||||
to rewrite it for you. But notice the comment above: it won't run
|
||||
the loop if it's already running, because it can't. That means this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def main():
|
||||
# 3. the loop is running here
|
||||
print(
|
||||
client.get_me() # 4. this will return a coroutine!
|
||||
.username # 5. this fails, coroutines don't have usernames
|
||||
)
|
||||
|
||||
loop.run_until_complete( # 2. run the loop and the ``main()`` coroutine
|
||||
main() # 1. calling ``async def`` "returns" a coroutine
|
||||
)
|
||||
|
||||
|
||||
Will fail. So if you're inside an ``async def``, then the loop is
|
||||
running, and if the loop is running, you must ``await`` things yourself:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def main():
|
||||
print((await client.get_me()).username)
|
||||
|
||||
loop.run_until_complete(main())
|
||||
|
||||
|
||||
What are async, await and coroutines?
|
||||
=====================================
|
||||
|
||||
The ``async`` keyword lets you define asynchronous functions,
|
||||
also known as coroutines, and also iterate over asynchronous
|
||||
loops or use ``async with``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
# ^ this declares the main() coroutine function
|
||||
|
||||
async with client:
|
||||
# ^ this is an asynchronous with block
|
||||
|
||||
async for message in client.iter_messages(chat):
|
||||
# ^ this is a for loop over an asynchronous generator
|
||||
|
||||
print(message.sender.username)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
# ^ this assigns the default event loop from the main thread to a variable
|
||||
|
||||
loop.run_until_complete(main())
|
||||
# ^ this runs the *entire* loop until the main() function finishes.
|
||||
# While the main() function does not finish, the loop will be running.
|
||||
# While the loop is running, you can't run it again.
|
||||
|
||||
|
||||
The ``await`` keyword blocks the *current* task, and the loop can run
|
||||
other tasks. Tasks can be thought of as "threads", since many can run
|
||||
concurrently:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
|
||||
async def hello(delay):
|
||||
await asyncio.sleep(delay) # await tells the loop this task is "busy"
|
||||
print('hello') # eventually the loop resumes the code here
|
||||
|
||||
async def world(delay):
|
||||
# the loop decides this method should run first
|
||||
await asyncio.sleep(delay) # await tells the loop this task is "busy"
|
||||
print('world') # eventually the loop finishes all tasks
|
||||
|
||||
loop = asyncio.get_event_loop() # get the default loop for the main thread
|
||||
loop.create_task(world(2)) # create the world task, passing 2 as delay
|
||||
loop.create_task(hello(delay=1)) # another task, but with delay 1
|
||||
try:
|
||||
# run the event loop forever; ctrl+c to stop it
|
||||
# we could also run the loop for three seconds:
|
||||
# loop.run_until_complete(asyncio.sleep(3))
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
The same example, but without the comment noise:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
|
||||
async def hello(delay):
|
||||
await asyncio.sleep(delay)
|
||||
print('hello')
|
||||
|
||||
async def world(delay):
|
||||
await asyncio.sleep(delay)
|
||||
print('world')
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(world(2))
|
||||
loop.create_task(hello(1))
|
||||
loop.run_until_complete(asyncio.sleep(3))
|
||||
|
||||
|
||||
Can I use threads?
|
||||
==================
|
||||
|
||||
Yes, you can, but you must understand that the loops themselves are
|
||||
not thread safe. and you must be sure to know what is happening. You
|
||||
may want to create a loop in a new thread and make sure to pass it to
|
||||
the client:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
def go():
|
||||
loop = asyncio.new_event_loop()
|
||||
client = TelegramClient(..., loop=loop)
|
||||
...
|
||||
|
||||
threading.Thread(target=go).start()
|
||||
|
||||
|
||||
Generally, **you don't need threads** unless you know what you're doing.
|
||||
Just create another task, as shown above. If you're using the Telethon
|
||||
with a library that uses threads, you must be careful to use ``threading.Lock``
|
||||
whenever you use the client, or enable the compatible mode. For that, see
|
||||
:ref:`compatibility-and-convenience`.
|
||||
|
||||
You may have seen this error:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
RuntimeError: There is no current event loop in thread 'Thread-1'.
|
||||
|
||||
It just means you didn't create a loop for that thread, and if you don't
|
||||
pass a loop when creating the client, it uses ``asyncio.get_event_loop()``,
|
||||
which only works in the main thread.
|
||||
|
||||
|
||||
client.run_until_disconnected() blocks!
|
||||
=======================================
|
||||
|
||||
All of what `client.run_until_disconnected()
|
||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` does is
|
||||
run the asyncio_'s event loop until the client is disconnected. That means
|
||||
*the loop is running*. And if the loop is running, it will run all the tasks
|
||||
in it. So if you want to run *other* code, create tasks for it:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
async def clock():
|
||||
while True:
|
||||
print('The time:', datetime.now())
|
||||
await asyncio.sleep(1)
|
||||
|
||||
loop.create_task(clock())
|
||||
...
|
||||
client.run_until_disconnected()
|
||||
|
||||
This creates a task for a clock that prints the time every second.
|
||||
You don't need to use `client.run_until_disconnected()
|
||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` either!
|
||||
You just need to make the loop is running, somehow. ``asyncio.run_forever``
|
||||
and ``asyncio.run_until_complete`` can also be used to run the loop, and
|
||||
Telethon will be happy with any approach.
|
||||
|
||||
Of course, there are better tools to run code hourly or daily, see below.
|
||||
|
||||
|
||||
What else can asyncio do?
|
||||
=========================
|
||||
|
||||
Asynchronous IO is a really powerful tool, as we've seen. There are plenty
|
||||
of other useful libraries that also use asyncio_ and that you can integrate
|
||||
with Telethon.
|
||||
|
||||
* `aiohttp <https://github.com/aio-libs/aiohttp>`_ is like the infamous
|
||||
`requests <https://github.com/requests/requests/>`_ but asynchronous.
|
||||
* `quart <https://gitlab.com/pgjones/quart>`_ is an asynchronous alternative
|
||||
to `Flask <http://flask.pocoo.org/>`_.
|
||||
* `aiocron <https://github.com/gawel/aiocron>`_ lets you schedule things
|
||||
to run things at a desired time, or run some tasks hourly, daily, etc.
|
||||
|
||||
And of course, `asyncio <https://docs.python.org/3/library/asyncio.html>`_
|
||||
itself! It has a lot of methods that let you do nice things. For example,
|
||||
you can run requests in parallel:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def main():
|
||||
last, sent, download_path = await asyncio.gather(
|
||||
client.get_messages('TelethonChat', 10),
|
||||
client.send_message('TelethonOfftopic', 'Hey guys!'),
|
||||
client.download_profile_photo('TelethonChat')
|
||||
)
|
||||
|
||||
loop.run_until_complete(main())
|
||||
|
||||
|
||||
This code will get the 10 last messages from `@TelethonChat
|
||||
<https://t.me/TelethonChat>`_, send one to `@TelethonOfftopic
|
||||
<https://t.me/TelethonOfftopic>`_, and also download the profile
|
||||
photo of the main group. asyncio_ will run all these three tasks
|
||||
at the same time. You can run all the tasks you want this way.
|
||||
|
||||
A different way would be:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
loop.create_task(client.get_messages('TelethonChat', 10))
|
||||
loop.create_task(client.send_message('TelethonOfftopic', 'Hey guys!'))
|
||||
loop.create_task(client.download_profile_photo('TelethonChat'))
|
||||
|
||||
They will run in the background as long as the loop is running too.
|
||||
|
||||
You can also `start an asyncio server
|
||||
<https://docs.python.org/3/library/asyncio-stream.html#asyncio.start_server>`_
|
||||
in the main script, and from another script, `connect to it
|
||||
<https://docs.python.org/3/library/asyncio-stream.html#asyncio.open_connection>`_
|
||||
to achieve `Inter-Process Communication
|
||||
<https://en.wikipedia.org/wiki/Inter-process_communication>`_.
|
||||
You can get as creative as you want. You can program anything you want.
|
||||
When you use a library, you're not limited to use only its methods. You can
|
||||
combine all the libraries you want. People seem to forget this simple fact!
|
||||
|
||||
|
||||
Why does client.start() work outside async?
|
||||
===========================================
|
||||
|
||||
Because it's so common that it's really convenient to offer said
|
||||
functionality by default. This means you can set up all your event
|
||||
handlers and start the client without worrying about loops at all.
|
||||
|
||||
Using the client in a ``with`` block, `start
|
||||
<telethon.client.auth.AuthMethods.start>`, `run_until_disconnected
|
||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>`, and
|
||||
`disconnect <telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`
|
||||
all support this.
|
||||
|
||||
Where can I read more?
|
||||
======================
|
||||
|
||||
`Check out my blog post
|
||||
<https://lonamiwebs.github.io/blog/asyncio/>`_ about asyncio_, which
|
||||
has some more examples and pictures to help you understand what happens
|
||||
when the loop runs.
|
||||
|
||||
.. _asyncio: https://docs.python.org/3/library/asyncio.html
|
Reference in New Issue
Block a user