-
Notifications
You must be signed in to change notification settings - Fork 0
Document the threading model from Tkinter's POV #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -91,54 +91,27 @@ Python interpreter. | |
| Threading model | ||
| --------------- | ||
|
|
||
| Python and Tcl/Tk have very different threading models, which :mod:`tkinter` | ||
| tries to bridge. If you use threads, you may need to be aware of this. | ||
|
|
||
| A Python interpreter may have many threads associated with it. In Tcl, multiple | ||
| threads can be created, but each thread has a separate Tcl interpreter instance | ||
| associated with it. Threads can also create more than one interpreter instance, | ||
| though each interpreter instance can be used only by the one thread that created it. | ||
|
|
||
| Each :class:`Tk` object created by :mod:`tkinter` contains a Tcl interpreter. | ||
| It also keeps track of which thread created that interpreter. Calls to | ||
| :mod:`tkinter` can be made from any Python thread. Internally, if a call comes | ||
| from a thread other than the one that created the :class:`Tk` object, an event | ||
| is posted to the interpreter's event queue, and when executed, the result is | ||
| returned to the calling Python thread. | ||
|
|
||
| Tcl/Tk applications are normally event-driven, meaning that after initialization, | ||
| the interpreter runs an event loop (i.e. :func:`Tk.mainloop`) and responds to events. | ||
| Because it is single-threaded, event handlers must respond quickly, otherwise they | ||
| will block other events from being processed. To avoid this, any long-running | ||
| computations should not run in an event handler, but are either broken into smaller | ||
| pieces using timers, or run in another thread. This is different from many GUI | ||
| toolkits where the GUI runs in a completely separate thread from all application | ||
| code including event handlers. | ||
|
|
||
| If the Tcl interpreter is not running the event loop and processing events, any | ||
| :mod:`tkinter` calls made from threads other than the one running the Tcl | ||
| interpreter will fail. | ||
|
|
||
| A number of special cases exist: | ||
|
|
||
| * Tcl/Tk libraries can be built so they are not thread-aware. In this case, | ||
| :mod:`tkinter` calls the library from the originating Python thread, even | ||
| if this is different than the thread that created the Tcl interpreter. A global | ||
| lock ensures only one call occurs at a time. | ||
|
|
||
| * While :mod:`tkinter` allows you to create more than one instance of a :class:`Tk` | ||
| object (with it's own interpreter), all interpreters that are part of the same | ||
| thread share a common event queue, which gets ugly fast. In practice, don't create | ||
| more than one instance of :class:`Tk` at a time. Otherwise, it's best to create | ||
| them in separate threads and ensure you're running a thread-aware Tcl/Tk build. | ||
|
|
||
| * Blocking event handlers are not the only way to prevent the Tcl interpreter from | ||
| reentering the event loop. It is even possible to run multiple nested event loops | ||
| or abandon the event loop entirely. If you're doing anything tricky when it comes | ||
| to events or threads, be aware of these possibilities. | ||
|
|
||
| * There are a few select :mod:`tkinter` functions that presently work only when | ||
| called from the thread that created the Tcl interpreter. | ||
| Tkinter strives to allow any calls to its API from any Python threads, without any limits, as expected from a Python module. Due to Tcl's architectural restrictions, however, that stem from its vastly different threading model, this is not always possible. | ||
|
|
||
| Tcl's execution model is based on cooperative multitasking. Control is passed between multiple interpreter instances by sending events (see `event-oriented programming -- Tcl/Tk wiki <https://wiki.tcl.tk/1772>`_ for details). | ||
|
|
||
| A Tcl interpreter instance has only one stream of execution and, unlike many other GUI toolkits, Tcl/Tk doesn't provide a blocking event loop. Instead, Tcl code is supposed to pump the event queue by hand at strategic | ||
| moments (save for events that are generated explicitly in the same OS thread -- these are handled immediately by simply passing control from sender to the handler). As such, all Tcl commands are designed to work without an event loop running -- only the event handlers will not fire until the queue is processed. | ||
|
|
||
|
||
| In multithreaded environments like Python, the common GUI execution model is rather to use a blocking event loop and a dedicated OS thread (called the "UI thread") to run it constantly. Usually, the main thread does this after doing the initialization. Other threads send work items (events) to its event queue when they need to do something in the GUI. Likewise, for any lengthy tasks, the UI thread can launch worker threads that report back on their progress via the same event queue. | ||
|
|
||
| Tkinter implements the multithreaded model as the primary one, but it supports pumping events by hand instead of running the event loop, too. | ||
|
|
||
| Contrary to most GUI toolkits using the multithreaded model, Tkinter calls can be made from any threads -- even worker threads. Conceptually, this can be seen as the worker thread sending an event referencing an appropriate payload, and waiting for its processing. The implementation, however, can sometimes use a shortcut here. | ||
|
|
||
| * In threaded Tcl, an interpreter instance, when created, becomes tied to the creating OS thread. Any calls to this interpreter must come from this thread (apart from special inter-thread communication APIs). The upside is that calls to interpreters tied to different threads can run in parallel. Tkinter implements calls from outside the intrpreter thread by constructing an event with an appropriate payload, sending it to the instance's queue via the inter-thread communication APIs and waiting for result. As a consequence: | ||
|
|
||
| * To make any calls from outside the interpreter thread, :func:`Tk.mainloop` must be running in the interpreter thread. If it isn't, :class:`RuntimeError` is raised. | ||
| * A few select functions can only be run in the interpreter thread. These are the functions that implement the event loop -- :func:`Tk.mainloop`, :func:`Tk.dooneevent`, :func:`Tk.update`, :func:`Tk.update_ideltasks` -- and :func:`Tk.destroy` that terminates it halfway through. | ||
|
|
||
| * For non-threaded Tcl, threads effectively don't exist. So, any Tkinter call is carried out in the calling thread, whatever it happens to be (see :func:`Tk.mainloop`'s entry on how it is implemented in this case). Since Tcl has a single stream of execution, all Tkinter calls are wrapped with a global lock to enforce sequential access. So, in this case, there are no restrictions on calls whatsoever, but only one call, to any interpreter, can be active at a time. | ||
|
|
||
| The last thing to note is that Tcl event queus are not per-interpreter but rather per-thread. So, a running event loop will process events not only for its own interpreter, but also for any others that share the same thread. This is transparent for the code though because an event handler is invoked within the context of the correct interpreter (and in the correct Python lexical context if the handler has a Python payload). There's also no harm in trying to run an event loop for two interpreters that may happen to share a queue: in threaded Tcl, such a clash is flat-out impossible because they would have to both run in the same OS thread, and in non-threaded Tcl, they would take turns processing events. | ||
|
||
|
|
||
|
|
||
| Module contents | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the multiple interpreter thing is at all relevant. Main point.. one thread, event-oriented programming model. Calling Tk.mainloop causes Tcl to grab the next event, and process it via an event handler. Once that event handler finishes, the next event can be grabbed, etc. If for some reason that event handler takes a long time to run, there could be a long period of time before the next event is processed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To amplify this, in 99.99% of Tkinter apps, which would have one instance of Tkinter(), there's only ever going to be a single Tcl interpreter.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes and no. Cooperative multitasking is all about multiple independent agents coexisting in the same thread (in fact, for the purpose of cooperative multitasking, threads are irrelevant) and passing control between each other in some semi-predictable manner. Passing messages (which is what "events" effectively are) is just a very convenient way to do so that was popularized by Smalltalk back then AFAIK.
So, events are not just "signals that something happened", they both pass information and control.
In batch computations on a single-processor system, this theoretically gives the same benefits as multithreading, without the overhead. That's why no thread support in Tcl was seen as a no problem or even benefitial. (If the system needs to be responsive to external events that can arrive at any moment, this is not so "good" anymore... oh, well :) )
Multiple threads in Tcl were actually "slapped" on top of this machinery. Just like in a single thread, one instance sends a "message" to another and waits for the result. While that other one is checking for incoming messages this way or another. So, an inter-thread message still effectively leads to "passing control" between the two instances.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to explain all this in the text, and it invariably came across as irrelevant and confusing. That's why I ultimately limited the text to mentioning just the most key an unique trait of cooperating multitasking (so the reader gets the best idea and doesn't wonder how it is relevant to "cooperative multitasking" specifically) -- passing control.
The fact that the event queue is also used to react to external input is less critical and much more obvious -- and I focus on this right in the next paragraph, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considered "Information and control are passed...".
We need to somehow emphasize that they are passed together though, which this doesn't do.
Since "event" implies that it carries some data, I think the current formulation gives the most accurate impression in the end.