Perl Threading
Written by Nikos Vaggalis   
Wednesday, 06 April 2011
Article Index
Perl Threading
Components of UE&R
From single-threaded to multi-threaded
The message loop

 

Message loop

The heart of the GUI is its message loop.

(gui.pl)
sub init_gui {
 gui::userinit() if defined &gui::userinit
$top = MainWindow->new(-background =>ivory4);
$top->title("Unrar Extract & Recover 4.0 ANSI
edition - http://unrarextractrec.
sourceforge.net/" );
$top->resizable(0,0);
gui::ui($top);
gui::run() if defined &gui::run;
gui::load();
Tk::MainLoop();

When Tk::MainLoop() is reached , the GUI gets in a state of continuously looking for events; when there is one, it dispatches it to the appropriate event handler.

However Tk::MainLoop() can be expanded into :

while (Tk::MainWindow->Count) {
 DoOneEvent(ALL_EVENTS);
}

So if I have access to a global while loop that continuously checks for events, why not use that to my advantage? Hence I hook into the main loop itself it and check for any thread queued messages with non-blocking dequeue_nb instead of polling

(gui.pl)
while (Tk::MainWindow->Count) {
if (my $queue_message=$main::
worker_to_boss_queue->dequeue_nb) {
my ($message,$no)=@$queue_message;
given ($message) {
when ("allfiles") {
$gui::percent_done=0;
$gui::progress->configure(-to => $no);
}
when ("update") {
$gui::percent_done += 1;
}
when ("end") {
$gui::percent_done += 1;
&enable_buttons;
}
}
}
DoOneEvent(ALL_EVENTS);
}


Expect the reason for coordination, an additional reason for using the thread queue is combating thread-affinity.

When the worker thread finishes processing a file (successfully or unsuccessfully that is not the issue) it not only has to communicate the result back to the main thread, but also has to update the progress bar. Because of thread-affinity, all visual controls belong to the GUI thread and cannot be touched by our worker thread. Therefore when the main thread receives a message from the worker thread it also uses it to update the progress bar:

$gui::percent_done += 1

Pausing operation

The callback proved handy again. Now there is no need to wrap the update() method anymore but we use it for pausing the worker thread. The callback now wraps the pause method of a Win32 Event.

(Unrar_Extract_and_Recover.pl)
$event=Win32::Event->new(1,"uer_pause_event");
$event->set;
my $callback=sub { $event->wait() };

When the user clicks 'Pause', which is in the main GUI thread's responsibility, a Win32 event is signalled which indicates that the worker should take a break from what it is doing. The worker thread checks inside the same loop as before for the signalling of the event and pauses the operation if it does.

(gui.pl)
sub pause {
$main::event->reset;
sleep 1;
print "\n\n.......PAUSED.......\n\n";
$gui::pause->configure(-relief=>"sunken");
}
(Unrar.pm)
while ( ( $RAR_functions{RARReadHeader}->
Call( $handle, $RARHeaderData_struct ))
 == 0 ){
$blockencryptedflag="yes";
$callback->(@_) if defined($callback);

On the other hand when the user presses 'Resume' the event is switched to non-signalled state and the worker threads wakes up and resumes processing. A clean solution.

(gui.pl)
sub resume {
print "\n\n......RESUMED.....\n\n";
$main::event->set;
$gui::pause->configure(-relief=>"raised");
}

The good thing about a Win32 event is that it is a kernel object which means that it is visible by all threads (it is also visible by other processes hence it has to have a unique identifying name to avoid name collision) and is immediately visible by the worker thread with no need to coordinate anything

All fine but there is a fundamental issue with Perl threading; it uses TLS (thread local storage) and when a thread is spawn it actually forks the whole interpreter which means that all state is duplicated into the new thread. All global variables and contents of the heap are not shared but are cloned seamlessly into the new thread (to actually share a variable or structure you have to explicitly share).

This in effect means that (expect of the increased memory consumption) if the thread is spawned after the Tk window is active, then also the Tk window structure will be cloned into the new thread. However the handle of the original Tk window is a kernel object and thus not cloned; we now have one invisible orphan window.

Hence it is common practice to spawn a thread before you create something that you do not want to be cloned. That is also the reason that we spawn the worker thread before we create the Tk window. Another issue with Tk is that its controls do not exchange windows messages (WM_) with native windows MFC controls correctly, which poses issues with drawing/painting the GUI such as this:

 

mfc

 

Here a button is clicked on the Tk main window which fires the MFC windows control for browsing directories (ShBrowseForFolder). Everything seems fine until you drag the control around which does not refresh the background.

The way to handle it would be, again, to hook into the Tk Windows Procedure, capture the messages (especially WM_PAINT, WM_ERASEBKGND) and handle them manually.

Summary

So at the end of the day, what have we achieved by utilizing threading? We not only have a fully responsive GUI, no freezing, no waiting and more user satisfaction as an outcome but also cleaner more maintainable code.

 




Last Updated ( Friday, 19 August 2011 )
 
 

   
RSS feed of all content
I Programmer - full contents
Copyright © 2014 i-programmer.info. All Rights Reserved.
Joomla! is Free Software released under the GNU/GPL License.