Skip to content

Avoid polling in favour of Events - #7

Closed
glglgl-de wants to merge 2 commits into
asyncgui:mainfrom
glglgl-de:no-polling
Closed

Avoid polling in favour of Events#7
glglgl-de wants to merge 2 commits into
asyncgui:mainfrom
glglgl-de:no-polling

Conversation

@glglgl-de

Copy link
Copy Markdown

run_in_thread() and run_in_executor() start their job and then go into a polling mode where they check the "done" flag in a regular fashion.

This is not very elegant, considering the fact that we have beautiful event classes at hand which are much better to be used for waiting.

glglgl-de added 2 commits July 2, 2026 17:05
run_in_thread() and run_in_executor() start their job and then go into
a polling mode where they check the "done" flag in a regular fashion.

This is not very elegant, considering the fact that we have beautiful
event classes at hand.

So use them for waiting.
Instead of handling result and exceptoin on our own, make use of
the fact that an executor passes the result or exception of the called
function into the future. This way, we only need to have the future call
event.fire() over thread boundaries (using after(0, ...)) and then can use
the future to get retuen value or exception as it is available.
@glglgl-de

Copy link
Copy Markdown
Author

This one I leave in draft mode as I didn't test it very well until now.

@gottadiveintopython

gottadiveintopython commented Jul 4, 2026

Copy link
Copy Markdown
Member

With this change, after is called from outside the thread running Tkinter, which may break the program either silently or otherwise. Anything that isn't explicitly documented as thread-safe should be assumed not to be thread-safe, and Tkinter doesn't make such a guarantee. In short, you should not interact with Tkinter from any thread other than the one running it.

Perhaps this could be made safe using threading.Lock or similar synchronization primitives, but ChatGPT suggested that wouldn't work. Therefore, polling is the only approach I can think of at the moment.

Thank you for your contribution.

@glglgl-de

glglgl-de commented Jul 4, 2026

Copy link
Copy Markdown
Author

With this change, after is called from outside the thread running Tkinter, which may break the program either silently or otherwise. Anything that isn't explicitly documented as thread-safe should be assumed not to be thread-safe, and Tkinter doesn't make such a guarantee.

In https://docs.python.org/3/library/tkinter.html#threading-model, it says

Calls to tkinter can be made from any Python thread. Internally, if a call comes from a thread other than the one that created the Tk object, an event is posted to the interpreter’s event queue, and when executed, the result is returned to the calling Python thread.

I would not dare to test this on every method possible, but especially for .after() and .after_idle(), I have the impression that this is the usual way to go for passing things from other threads to the Tkinter thread.

@gottadiveintopython

gottadiveintopython commented Jul 6, 2026

Copy link
Copy Markdown
Member

I didn't realize it was officially documented as thread-safe. That's news to me.

I once tried combining Tkinter with Trio using Trio's Guest Mode, which requires a thread-safe way to schedule a function from a worker thread to be executed on the main thread. I first tried the after method directly, but that didn't work, so I ended up using queue.Queue, which is thread-safe. This is the code I ended up with.

I just tried it again, and this time it worked without relying on queue.Queue. I'm not sure why it didn't work back then, as many things have changed since then (the Python version, the Trio version, etc.). But since it now seems to work, I'll trust what you said.

There are a couple of things I'd like to ask:

  • There seems to be a way to check whether Tkinter was compiled with thread support:

    def _check_thread_safety(widget):
        if not widget.call("set", "tcl_platform(threaded)"):
            raise Exception("Tkinter is not compiled with thread support")

    But I can't tell from the documentation whether this kind of check is necessary . Do you think it is? If so, could you please add one?

  • Since this pr introduces a breaking API change, I'd like you to add a versionchanged note:

    .. versionchanged:: 0.3.0
        ...
  • As for run_in_executor, Extend run_in_executor to support more Executor implementations #8 adds support for other Executor implementations, which means Python objects can no longer be passed directly to and from workers, since they may need to be serialized and deserialized. Given that, could we revert the run_in_executor API changes in this pr and leave them to that pr instead?

  • Do you think after_idle(...) would be better than after(0, ...) here? If so, could you replace it? We're already breaking the API by removing the polling_interval_ms parameter, so it's a good opportunity to make other breaking changes as well. I don't know which approach is better myself, since I don't use Tkinter.

@gottadiveintopython

Copy link
Copy Markdown
Member

which means Python objects can no longer be passed directly to and from workers, since they may need to be serialized and deserialized.

Sorry, I may be wrong. As long as you rely on Future.add_done_callback() as this pr does, you don't need to worry about serialization, and Future handles it automatically. Am I understanding that correctly?

@glglgl-de

glglgl-de commented Jul 6, 2026

Copy link
Copy Markdown
Author

which means Python objects can no longer be passed directly to and from workers, since they may need to be serialized and deserialized.

Sorry, I may be wrong. As long as you rely on Future.add_done_callback() as this pr does, you don't need to worry about serialization, and Future handles it automatically. Am I understanding that correctly?

Yes, Future handles that automatically in this case. Serialization and deserialization would be needed if I wanted the Tk library to pass around things, as it is obviously addressed via text commands. But in this case, the Tk library only passes around the IDs of the callables (as far as I can tell), and the objects remain in the Python domain.

BTW, in another documentation (https://tkdocs.com/tutorial/eventloop.html#threads), they seem to support your original standpoint, that for triggering stuff from other threads, the better way would be to use root.event_generate("<<MyOwnEvent>>") for generating an event and root.bind() to capture it in the GUI thread. Maybe that would be an approach if the other, simpler approach turns out not to work.

Resorting to polling still gives me a feeling of imperfection, somehow.

I am still going to play around with these things and will come back in a few days after I'll have hopefully found some answers to your questions and to my own ones.

Edit:

It seems to me that event_generate() and after() both are passed via the same path: self.tk.call(), which uses the same thread border crossing queue and which makes after() as safe as bind() / event_generate() and more uncomplicated.

I hope I dont talk too much uncoordinated things and it makes a little bit sense.

@glglgl-de

glglgl-de commented Jul 6, 2026

Copy link
Copy Markdown
Author

Oh, and about your questions:

There are a couple of things I'd like to ask:

  1. Check whether thread support is enabled

I'll try to find out whether this is actually necessary, and if so, if it is necessary for both variants.

Edit: According to this line , Tcl 9.0 and newer are always threaded, older versions are under certain conditions - they seem to make the same test as you suggest.

I found out that this test isn't foolproof, however - it is possible to manipulate the value in this array:

r.call("set", "tcl_platform(threaded)", "0") # and even
r.call("set", "tcl_platform(threaded)", "8")

both work and seem to alter the content of this array.

(But who does this, deserves the consequences.)

  1. versionchanged:: 0.3.0

I will do so in the next version of the PR.

  1. As for run_in_executor, Extend run_in_executor to support more Executor implementations #8 adds support for other Executor implementations, which means Python objects can no longer be passed directly to and from workers, since they may need to be serialized and deserialized. Given that, could we revert the run_in_executor API changes in this pr and leave them to that pr instead?

Although we (seem to) have consensus that it will work anyway, I think we should treat the futures object separately. That has the very big advantage that it is possible to get the futures from wherever we want - be it a "normal" executor from any flavour possible or if we get it from asyncio's run_coroutine_threadsafe, which also returns "concurrent" Futures. This would enables us to use one asyncio universe for GUI things (the asyncgui et al.), and use in other program parts asyncio code. That works as long as we make sure we don't mix any of them.

Do you think after_idle(...) would be better than after(0, ...) here? If so, could you replace it? We're already breaking the API by removing the polling_interval_ms parameter, so it's a good opportunity to make other breaking changes as well. I don't know which approach is better myself, since I don't use Tkinter.

That's an interesting question. after_idle() calls its callback after anything has been processed at the moment and the event queue is actually idle. after(0, ...), however, is called "as soon as possible". I think for the result of an external operation, the result might need to be available as soon as possible, so I tend to after(0, ...). But maybe it doesn't matter much, as it ideally is a question of only a few milliseconds.

@gottadiveintopython

Copy link
Copy Markdown
Member

Thank you for teaching me so much :) I think I'll leave the design of these two APIs to you, as you obviously know this area far better than I do.

@glglgl-de

Copy link
Copy Markdown
Author

Oof ... I didn't mean to press myself into this project, just wanted to make some suggestions. But if you think that would be ok, it would be an honour to me :-)

But then I'll have to think even more about it to make it clean and thoroughly test it.

(And the knowledge of this area is only a few weeks old and partially stems from ChatGPT, but I am very interested in how all this async stuff works :-))

@gottadiveintopython

gottadiveintopython commented Jul 6, 2026

Copy link
Copy Markdown
Member

Only a few weeks? That's even more impressive.

I am very interested in how all this async stuff works

My library, asyncgui, is definitely a good place to start because it's much simpler than asyncio or trio. But if that's still not simple enough, I recommend starting with an even simpler exercise: try implementing a mechanism that pauses the execution of a generator for the amount of time indicated by each number it yields, without freezing the Tkinter UI. Toga(GUI library) supports this kind of generator, as shown in this video.

It should take fewer than 10 lines of code, and that's how I started learning how generators and coroutines work. (generators and coroutines are almost the same).

(Sorry for the late reply. I can read English fairly quickly, but writing it takes me much longer.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants