2018-10-06 21:20:11 +03:00
|
|
|
.. _mastering-asyncio:
|
|
|
|
|
|
|
|
=================
|
|
|
|
Mastering asyncio
|
|
|
|
=================
|
|
|
|
|
|
|
|
.. contents::
|
|
|
|
|
|
|
|
|
|
|
|
What's asyncio?
|
2019-05-09 13:24:37 +03:00
|
|
|
===============
|
2018-10-06 21:20:11 +03:00
|
|
|
|
|
|
|
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?
|
2019-05-09 13:24:37 +03:00
|
|
|
============
|
2018-10-06 21:20:11 +03:00
|
|
|
|
|
|
|
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?
|
2019-05-09 13:24:37 +03:00
|
|
|
========================
|
2018-10-06 21:20:11 +03:00
|
|
|
|
|
|
|
.. 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?
|
2019-05-09 13:24:37 +03:00
|
|
|
===========================
|
2018-10-06 21:20:11 +03:00
|
|
|
|
|
|
|
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?
|
2019-05-09 13:24:37 +03:00
|
|
|
=====================================
|
2018-10-06 21:20:11 +03:00
|
|
|
|
|
|
|
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?
|
2019-05-09 13:24:37 +03:00
|
|
|
==================
|
2018-10-06 21:20:11 +03:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2018-10-07 12:18:40 +03:00
|
|
|
|
|
|
|
client.run_until_disconnected() blocks!
|
2019-05-09 13:24:37 +03:00
|
|
|
=======================================
|
2018-10-07 12:18:40 +03:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
2018-10-06 21:20:11 +03:00
|
|
|
What else can asyncio do?
|
2019-05-09 13:24:37 +03:00
|
|
|
=========================
|
2018-10-06 21:20:11 +03:00
|
|
|
|
|
|
|
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
|
2018-10-07 12:18:40 +03:00
|
|
|
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.
|
2018-10-06 21:20:11 +03:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2018-10-07 12:18:40 +03:00
|
|
|
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!
|
|
|
|
|
2018-10-06 21:20:11 +03:00
|
|
|
|
|
|
|
Why does client.start() work outside async?
|
2019-05-09 13:24:37 +03:00
|
|
|
===========================================
|
2018-10-06 21:20:11 +03:00
|
|
|
|
|
|
|
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?
|
2019-05-09 13:24:37 +03:00
|
|
|
======================
|
2018-10-06 21:20:11 +03:00
|
|
|
|
|
|
|
`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
|