Combining Traditional Thread-Based Code and Asyncio in Python | by Peng Qian | May, 2023

Combining Traditional Thread-Based Code and Asyncio in Python | by Peng Qian | May, 2023

[ad_1]

There is another case where our program already implements a loop in the existing code. For example, most GUI programs use an event loop to respond to various events and to update the UI.

Let’s take tkinter as an example. tkinter will start a main loop when it starts, and this main loop will block the main thread and keep on looping. As shown in the figure below:

How does tkinter main loop works
How does the tkinter main loop work. Image by Author

A direct call to synchronous IO code will block the main loop

Let’s take the example of a tkinter program that contains a button and a status text:

This program uses a state machine to implement it. Every 60 milliseconds, the code refreshes the corresponding text according to the program’s current state.

When we click the request_code button, the workflow should ideally look like the following diagram:

Workflow of the tkinter program. Image by Author

But by the execution result, the program hangs when clicking the button, and the status text is updated until the IO blocking code finishes executing. It means that the main loop is blocked when the IO request is running, causing the GUI interface to be unresponsive:

The app is blocked and doesn’t show query text.
The app is blocked and doesn’t show query text. Image by Author

Using asyncio.run to Run Asyncio Code

Can we replace the requests package with the aiohttp package to achieve the asynchronous invocation of IO requests?

Here we first inherit the App class to implement a new class AppAsyncBase. In this new class, we use aiohttp to implement an async_request method to lay the foundation for subsequent asynchronous calls:

Readers of my previous article will know we can execute asynchronous methods inside synchronous code via asyncio.run:

Then, we implement a new class AppAsyncRun, by inheriting AppAsyncBase. In this new class, we override the request_remote method and use asyncio.run to call the async_request method directly:

Next, let’s look at the results. Because asyncio’s event loop is executed in the main thread by default, and when the event loop is running, it blocks the main thread, and the main loop of tkinter is blocked and unresponsive:

asyncio.run blocks the main loop.
asyncio.run blocks the main loop. Image by Author

Integrating Asyncio with Thread-Based Programs

Is there a way to solve the event loop blocking problem?

Here we can use a separate daemon thread and then run the event loop into the daemon thread, so asyncio’s event loop will not block the main thread. The diagram is as follows:

Combining tkinter and asyncio loops.
Combining tkinter and asyncio loops. Image by Author

Looking at the code implementation, we first inherit the AppAsyncBase class to implement a new class AppEventLoop. Next, override the request_remote method and use asyncio.run_coroutine_threadsafe to call the async_request method in the event loop. Request method in the event loop. asyncio.run_coroutine_threadsafe is also thread-safe:

Implement a run_event_loop method to call the loop.run_forever in the thread:

Then, use the contextmanager decorator to manage the lifecycle of the daemon thread:

Finally, implement the event loop integration and the app launch in the main method, and let’s see the result:

The event loop running separately in the daemon thread no longer blocks.
The event loop running separately in the daemon thread no longer blocks. Image by Author

Perfect! Click the button, the status text is changed accordingly, the whole GUI interface runs smoothly, and IO calls do not block the GUI ever. Mission accomplished.

[ad_2]
Source link

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *