Introduction¶
The problem with existing libraries¶
There are a couple of async libraries for Python.
One, of course, is asyncio
from the standard library.
Another one is Trio, well-known for its structured concurrency.
And Curio, which seems to have had a big influence on Trio, though it now only accepts bug fixes.
They may be different each other but all of them have in common is that they are not suitable for GUI programs.
I’m not saying “Having a GUI library and an async library coexist in the same thread is problematic due to their individual main loops”.
In fact, Kivy and BeeWare have adapted themselves to work with async libraries,
PyGame doesn’t even own a main loop and expects the user to implement one so you could do that by putting an await asyncio.sleep()
inside a main loop [1],
tkinter
and PyQt seem to have 3rd party libraries that allow them to work with async libraries.
Even if none of them are your options, Trio has special mode that makes it run without interfering with other main loop.
Therefore, I think it’s safe to say that the coexisting problem has been already solved.
Then why do I say “they are not suitable for GUI programs”?
It’s because they cannot immediately start/resume tasks.
For instance, asyncio.create_task()
and asyncio.TaskGroup.create_task()
are the ones that start tasks in
asyncio
, but neither of them does it immediately.
Let me clarify what I mean by “immediately” just in case. It means the following test should pass:
import asyncio
flag = False
async def async_fn():
global flag; flag = True
async def main():
asyncio.create_task(async_fn())
assert flag
asyncio.run(main())
which does not.
The same applies to trio.Nursery.start()
and trio.Nursery.start_soon()
(This has “soon” in its name so it’s obvious).
The same issue arises when they resume tasks.
asyncio.Event.set()
and trio.Event.set()
don’t immediately resume the tasks waiting for it to happen.
They schedule the tasks to eventually resume, thus, the following test fails.
import asyncio
flag = False
async def async_fn(e):
e.set()
assert flag
async def main():
e = asyncio.Event()
asyncio.create_task(async_fn(e))
await e.wait()
global flag; flag = True
asyncio.run(main())
Why does the inability to immediately start/resume tasks make async libraries unsuitable for GUI programs? Take a look at the following pseudo code that changes the background color of a button while it is pressed.
async def toggle_button_background_color(button):
while True:
await button.to_be_pressed()
button.background_color = different_color
await button.to_be_released()
button.background_color = original_color
Consider a situation where the task is paused at the await button.to_be_pressed()
line and the user presses the button.
As I mentioned, neither of asyncio
nor trio
resumes tasks immediately, so the background color won’t change immediately.
Now, what happens if the user releases the button before the task resumes?
The task eventually resumes and pauses at the await button.to_be_released()
line…
but the user has already released the button.
The task ends up waiting there until the user presses and releases the button again.
As a result, the background color of the button remains the different_color
until that happens.
Note
The situation is worse in Kivy. In Kivy, touch events are stateful objects. If you fail to handle them promptly, their state might undergo changes, leaving no time to wait for tasks to resume.
Reacting to events without missing any occurrences is challenging for async libraries that cannot start/resume tasks immediately. The only solution I came up with is that, recording events using traditional callback APIs, and supplying them to the tasks that resume late a.k.a. buffering. I’m not sure it’s possible or practical, but it certainly has a non-small impact on performance.
If you use asyncgui
, that never be a problem.
asyncgui¶
Immediacy¶
The problem mentioned above doesn’t occur in asyncgui
because:
asyncgui.start()
andasyncgui.Nursery.start()
immediately start a task.asyncgui.Event.fire()
immediately resumes a task.
All other APIs work that way as well.
No main loop¶
The coexistence problem I mentioned earlier doesn’t occur in asyncgui
because it doesn’t have its own main loop.
Instead, asyncgui
runs by piggybacking on another main loop, such as one from a GUI library.
To achieve this, however, you need to wrap the callback-style APIs associated with the main loop it piggybacks.
I’ll explain this further in the Usage section.
Note
“another main loop” can be other async library’s.
Yes, you can even run asyncgui
and other async library in the same thread (though there are some limitations).
No global state¶
Although it wasn’t originally intended, asyncgui
ended up having no global state. All states are represented as:
free variables
local variables inside coroutines/generators
instance attributes
not:
module-level variables
class-level attributes
Note
Other async libraries have global states.
Cannot sleep by itself¶
It might surprise you, but asyncgui
cannot await sleep(...)
by itself.
This is because it requires a main loop, which asyncgui
lacks.
However, you can achieve this by wrapping the timer APIs of the main loop it piggybacks on, as I mentioned earlier.
In fact, that is the intended usage of this library.
asyncgui
itself only provides the features that depend solely on the Python language (or maybe some CPython-specific behavior),
and doesn’t provides the ones that need to interact with the operating system [2].