asyncio-glib icon indicating copy to clipboard operation
asyncio-glib copied to clipboard

Callback is never called when set_result() is called on a Future

Open talljosh opened this issue 5 years ago • 3 comments
trafficstars

When code triggered by a Gtk signal calls set_result() on an asyncio Future object, it seems the callbacks on the Future object are not called unless something else wakes up the main loop.

I've put together the following minimal example of this bug.

Expected behaviour: clicking the button should print, 'button clicked' and 'future finished', then the main loop should end. Actual behaviour: clicking the button prints 'button clicked', then the main loop continues forever. Tested with: Python 3.6.9, Gtk 3.22.30 and asyncio-glib 0.1.

If you uncomment the heartbeat line, so that there's a regular timed call happening, then the Future's callback is called and the main loop ends. But with that line commented, the loop does not end.

import asyncio
import asyncio_glib
from gi.repository import Gtk

asyncio.set_event_loop_policy(asyncio_glib.GLibEventLoopPolicy())


class Demo:
    def __init__(self):
        self.future = asyncio.get_event_loop().create_future()

        self.window = Gtk.Window()
        button = Gtk.Button.new_with_label('Click here')
        button.connect('clicked', self.button_clicked)
        self.window.add(button)
        self.window.show_all()

    def button_clicked(self, widget):
        print('button clicked')
        self.window.close()
        self.future.set_result(None)


async def main():
    # Uncomment the following line and everything works.
    # asyncio.get_event_loop().call_later(1, heartbeat)
    demo = Demo()
    await demo.future
    print('future finished')


def heartbeat():
    asyncio.get_event_loop().call_later(1, heartbeat)
    print('tick')


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

talljosh avatar Apr 11 '20 05:04 talljosh

This is essentially the problem I wrote about here:

https://blogs.gnome.org/jamesh/2019/10/07/gasyncresult-with-python-asyncio/

The basic problem is that GLib callbacks are outside of asyncio's view of what's happening on the thread. The work-around I used in that blog article was to treat these callbacks as if they were happening off-thread, and use call_soon_threadsafe as a way to schedule asyncio work. It's not pretty, but it gets the job done.

Part of the problem is that there's a lot more entry points that require passing control back to asyncio than just call_soon.

jhenstridge avatar Apr 11 '20 14:04 jhenstridge

What kind of entry points are we talking about? To me the obvious non-I/O ones are call_soon and call_later/call_at. Are there other categories of entry point that suffer from this issue?

The problem I see with your call_soon_threadsafe workaround is that it requires the user to know that they're using one of these unsupported entry points, and they're not obviously documented (at least, they're not mentioned in README.md). But it seems to me if we can document the entry points, why not just fix them in a similar manner to what I've submitted for call_soon?

But as I said above, I'm not entirely sure what entry points we're talking about, so maybe that's not feasible.

talljosh avatar Apr 12 '20 03:04 talljosh

I ran into the same issue - thanks for pointing out to the call_soon_threadsafe solution, that works file (although as you say it's far from pretty). It would be nice to explain this in the README so people don't have to dive into the tickets to find out how to get it working :)

Also, as this sort of problem does not appear in gbulb, it would be worth mentioning explicitly in the differences between the two tools.

ydirson avatar Apr 15 '20 11:04 ydirson