Tk::Threaded - Apartment threaded wrapper for Perl/Tk
#
# define an apartment threaded Model object
#
package ButtonHandler;
use Thread::Apartment::Server;
use base qw(Thread::Apartment::Server);
use strict;
use warnings;
sub new {
my ($class, $tac, $mw, $img) = @_;
my $self = bless {_mw => $mw, _img => $img }, $class;
$self->set_client($tac);
#
# register ourselves with Tk::Threaded
#
$mw->register(
-initialize => sub { $self->install(); },
-finalize => sub { $self->finalize(); });
return $self;
}
#
# called before Tk::Threaded enters MainLoop
#
sub install {
my $self = shift;
my ($mw, $img) = ($self->{_mw}, $self->{_img});
my $btn = $mw->Button(
-image => $img,
-command => sub { print "Button press!!!\n"; });
$btn->pack();
}
sub finalize {
print "We're all done!\n";
}
#
# usual TAS stuff...
#
sub get_simplex_methods {
return { install => 1 };
}
#
# now build the app
#
use Thread::Apartment;
#
# create a pool before installing Tk
#
Thread::Apartment->create_pool(AptPoolSize => 4);
#
# then load Tk::Threaded with any widget classes it needs
#
require Tk::Threaded qw(Tk::Button Tk::Photo);
import Tk::Threaded MainLoop;
#
# create MainWindow as usual
#
my $mw = MainWindow->new();
#
# use it as usual
#
my $img = $mw->Photo(-data => components_gif(), -format => 'gif');
#
# install another T::A object into the thread pool,
# passing it the Tk objects it needs
#
my $handler = Thread::Apartment->new(
AptClass => 'ButtonHandler',
AptParams => [ $mw, $img ]
);
#
# all set, so let her rip!
#
Tk::Threaded->MainLoop();
Tk::Threaded provides an apartment threading wrapper for Perl/Tk. In general, Tk is not threads-friendly:
Thread::Apartment provides all the neccesary plumbing to wrap Tk in the root thread, and expose all its methods via a client proxy, so that other Thread::Apartment objects can blissfully use Tk - even concurrently - without needing to implement their own threads-capable request queueing or closure proxying.
Tk::Threaded exposes nearly all the standard Tk methods, including the widget factory methods, as well as supporting passing and invoking closure arguments. Note that the objects returned from widget factory methods are actually proxy Thread::Apartment::Client objects, but behaviorally, applications shouldn't really be able to tell the difference from the "real thing" (with a few minor exceptions).
require'ing Widget PackagesApplications which use widgets outside of the "standard" widget set should specify them in a list in the require Tk::Threaded statement, e.g.,
require Tk::Threaded qw(Tk::Button Tk::Photo Tk::DynaTabFrame);
Note that the specified Tk packages are loaded into the Tk::Threaded::Server apartment thread, not the local thread in which the Tk::Threaded object is created.
Also, best practice is to create an Thread::Apartment thread pool for all the apartment threaded objects, including Tk::Threaded, prior to require'ing any other packages, in order to minimize the amount of context cloned into all the apartment threads.
Finally, be advised that it is unneccesary to use/require Tk widget packages into other apartment threads that create/use widgets. Instead, simply supply the full list of packages in the require Tk::Threaded list, or call the $mw->require(@package_list) method which will pull in the needed packages within the Tk::Threaded::Server thread.
The following options are available when applying Tk::Threaded:
For simple applications with minimal GUI interaction requirements, using Tk::Threaded alone as the View component may be sufficient. In such architectures, the application
require's Tk::Threaded, listing any additional widget packages it needs, and then import's MainLoop().MainWindow->new().Performing this step in a single phase prior to installing widgets into apartment threaded Model objects coordinates the layout behavior of Tk in a deterministic manner, as opposed to the non-deterministic order (from Tk's perspective) that might arise if each Model object concurrently injects its own set of widgets into the GUI. Also, some widgets do not behave/layout well if added to the GUI after the MainLoop has been started.
Model object constructors may make calls on the provided widget objects as needed, including creating and laying out new widgets. Assuming Tk::Threaded::MainLoop() has not yet been called, the widgets will be layed out as for a non-threaded execution.
Tk::Threaded::MainWindow::register() method, which accepts a named -finalize closure parameter. Registered -finalize closures are called in simplex context
- when MainLoop returns,
- when a Thread::Apartment STOP command is received
- when the MainWindow WM_DELETE_WINDOW callback is called.
Note that Tk::Threaded traps all external MainWindow->protocol(WM_DELETE_WINDOW, $closure) calls, and converts the $closure to a -finalize closure.
The finalizer provides a means for the external apartment threaded objects to perform any needed cleanup before the application exits.
That's all thats required to implement simple threaded GUI's with Tk::Threaded.
However, there are limitations:
:variable exports to directly reference the Tk::widget and Tk::event objects. The callbacks bound to events must use the input widget parameter, and acquire the events structure from that widget.For more advanced applications with complex GUI interaction requirements, Tk::Threaded provides the CoHabitant() factory method. This method permits View objects to be created and executed within Tk::Threaded's apartment thread, subject to the same thread governor as Tk::Threaded. Such objects avoid the overhead of proxied method calls, and support a more complete set of Tk functionality, without the more complicated coding required to create a complete Tk MegaWidget.
To install a View component into Tk::Threaded:
new() as its constructor. Note that the class should not derive from Thread::Apartment::Server, since Tk::Threaded::Server provides the thread governor and TAS behaviors.Tk::Scrolled() widget creation). CoHabitant() will return a proxy object for the installed object.By creating higher level View objects built on top of Tk, exposing high-level interfaces for Model objects, and installing the View objects within the Tk::Threaded apartment, the low-level GUI operations are executed entirely within the Tk::Threaded apartment, without the overhead of threads::shared data exchanges, and usage restrictions imposed by the simple proxied approach.
after(), repeat(), fileevent()While Tk::Threaded does support these async event dispatchers, they're really not needed for properly threaded architectures. With Tk::Threaded, there's no longer any need to interleave your timer or I/O events with Tk's control loop, or to even to worry about long running operations (unless, of course, you need some higher priority feedback from the GUI).
Instead, just write simple objects which block on I/O objects, or implement your own timer Thread::Apartment object, and let Tk::Threaded just handle the GUI operations.
Currently, several Tk widgets permit variable references to be used to communicate to the GUI (e.g., -textvariable, -variable configuration parameters). These interfaces rely on ties, which cannot be supported by threads::shared variables...which would be required to reference the tied variables from outside the Tk apartment thread. Some solution based on a Thread::Sociable extension to provide "trigger" variables may be possible.
The best way to prove Tk::Threaded's capability, and surface any issue, would be to convert the entire widgets demo...but thats a lot of code, and ideally would be restructured to uses a threaded architecture. Any volunteers?
Copyright(C) 2005, 2006, Dean Arnold, Presicient Corp., USA
Licensed under the Academic Free License version 2.1, as specified in the License.txt file included in this software package, or at OpenSource.org http://www.opensource.org/licenses/afl-2.1.php.