Using asyncio with Tkinter

Starting with Python 3.4, a new module named asyncio was introduced as a Python standard module.

The term Asyncio is made by adding two words: async + I/O.  Async is about concurrency, which means doing more than one thing at a time. I/O, on the other hand, refers to handling I/O bound tasks. A bound task means the thing that keeps your program busy.  If, for instance, you are doing computation-intensive math processing, the processor is taking most of the timeā€”and it is, therefore, a CPU bound task. On the contrary, if you are waiting for a result from the network, result from the database, or an input from the user, the task is I/O bound.

So in a nutshell, the asyncio module provides concurrency, particularly for I/O bound tasks. Concurrency ensures that you do not have to wait for I/O bound results.

Let's say you have to fetch content from multiple URLs, then process the fetched content to extract the title and display it in a Tkinter window. Now you obviously cannot fetch the content in the same thread that runs the Tkinter main loop, as that would make the root window unresponsive while the content is fetched.

So one of the options is to spawn a new thread for each URL. While this can be an option, it is not a very scalable one as spawning thousands or more threads at a time can lead to a lot of code complexity. We already saw a demo of a race condition in the beginning of the current chapter (9.01_race_condition.py), where running multiple threads can make it difficult to control the shared state. Furthermore, as context switching is an expensive and time-consuming affair, the program can become laggy after spawning just a few threads.

Here's where asyncio comes to our rescue. In contrast to multithreading, which relies on threading, asyncio uses a concept of event loops.

To demonstrate, here is a Tkinter program that on the click of a button simulates fetching 10 URLs:

from tkinter import Tk, Button
import asyncio
import threading
import random

def asyncio_thread(event_loop):
print('The tasks of fetching multiple URLs begins')
event_loop.run_until_complete(simulate_fetch_all_urls())

def execute_tasks_in_a_new_thread(event_loop):
""" Button-Event-Handler starting the asyncio part. """
threading.Thread(target=asyncio_thread, args=(event_loop, )).start()

async def simulate_fetch_one_url(url):
""" We simulate fetching of URL by sleeping for a random time """
seconds = random.randint(1, 8)
await asyncio.sleep(seconds)
return 'url: {} fetched in {} seconds'.format(url, seconds)

async def simulate_fetch_all_urls():
""" Creating and starting 10 i/o bound tasks. """
all_tasks = [simulate_fetch_one_url(url) for url in range(10)]
completed, pending = await asyncio.wait(all_tasks)
results = [task.result() for task in completed]
print(' '.join(results))

def check_if_button_freezed():
print('This button is responsive even when a list of i/o tasks are in progress')

def main(event_loop):
root = Tk()
Button( master=root, text='Fetch All URLs',
command=lambda: execute_tasks_in_a_new_thread(event_loop)).pack()
Button(master=root, text='This will not Freeze',
command=check_if_button_freezed).pack()
root.mainloop()

if __name__ == '__main__':
event_loop = asyncio.get_event_loop()
main(event_loop)

Here's a brief description of the code (9.12_async_demo.py):

  • The first step in using the asyncio module is to construct an event loop using the code event_loop = asyncio.get_event_loop(). Internally, this event_loop will schedule all tasks assigned to it using coroutines and futures to do the I/O bound tasks in an asynchronous manner.
  • We pass this event_loop as an argument to the Tkinter root window, so that it can use this event loop for scheduling async tasks.
  • The method that is in charge of doing the I/O bound task is then defined by appending the keyword async in front of the method definition. Essentially, any method that is to be executed from the event loop must be appended with the keyword async.
  • The method simulates a time-taking I/O blocking task using await asyncio.sleep(sec)In a real case, you will perhaps use this to fetch the contents of a URL or perform a similar I/O blocking task.
  • We start executing the async tasks in a new thread. This single thread executes the list of tasks using the event_loop.run_until_complete(simulate_fetch_all_urls()) command. Note that this is different from creating one thread each for each of the tasks. In this case, we are only creating a single thread to isolate it from the Tkinter main loop.
  • The line all_tasks = [simulate_fetch_one_url(url) for url in range(10)] combines all the async tasks into a list. This list of all I/O bound tasks is then passed on to completed, pending = await asyncio.wait(all_tasks), which waits for all tasks to be completed in a non-blocking manner. Once all the tasks are completed, the results are populated in the completed variable.
  • We get the results of individual tasks using results = [task.result() for task in completed].
  • We finally print out all the results to the console.

The benefit of using asyncio is that we do not have to spawn one thread for each task and as a result, the code does not have to context switch for each individual task.  Thus, using asyncio we can scale up to fetch thousands of URLs without slowing down our program and without worrying about managing results from each thread individually. 

This concludes our brief discussion on using the asyncio module with Tkinter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.75.165