q! Hi everyone,
This is a long message describing what I've learned about Win32::GUI, threads, and the DBI (DBD::mysql in my case). First of all, I want to thank the authors and contributors to this great Win32::GUI module. I've been using the module for a couple of weeks now on a side programming job I have. In the spirit of giving back, I'd like to share a solution that I've come across regarding a multi-threaded Win32-GUI app with a worker thread that makes DB connections, does other work, and updates the GUI with its progress. I hope this can help someone. === The Problem === I want a multi-threaded GUI application because I have long queries that I need to run and I still want my GUI to be responsive to the user. With a single threaded application, while the thread is waiting on a query to complete, the GUI freezes up and becomes unresponsive. To solve this issue, I first tried to use the Win32::GUI::ThreadUtils module. (http://www.robmay.me.uk/win32gui/ BTW, thanks to Mr. Rob May. I'm not using ThreadUtils but I am using your really cool SplashScreen module.) I can get the ThreadUtils examples to work, but when I start mixing in DB connections, that's when things start to get hairy. When using threads and the MySQL DBD, I seem to have issues. The connection works fine when instantiated, but when the script exits, there is a garbage collection error: Free to wrong pool during global destruction. (Resulting in one of those horrible Microsoft Send Error Report popups.) This does not make a professional looking application. Here's an example of the threaded DBD::mysql problem: <perlcode> use strict; use threads; use DBI; my $thr = threads->new(\&worker); $thr->join; print "Uh-oh\n"; sub worker { my $dbi = DBI->connect( 'dbi:mysql:test', 'user', 'pass' ); return 0; } </perlcode> === My Solution === Strangely enough, fork() works where perl threads don't. I don't know the details of why this is. I say strangely because it is my understanding that fork is emulated on Windows by using perl threads. (See http://search.cpan.org/~nwclark/perl-5.8.8/pod/perlfork.pod). I've found that a fork()ed pseudo-process that has its own DB connection doesn't die with the same garbage collection error as threads do when exiting. So, my setup is to have a worker fork()ed pseudo-process that handles all DB connections. The worker is pre-forked (after the GUI is set up) so that there is no delay when the user makes a request. The GUI thread communicates with the worker via a one-way pipe. It is my understanding that this works as the the fork()ed worker pseudo-process gets a copy of all the GUI handles and makes its own DB handles. GUI handles can be shared among threads, but DB handles can't be shared. Compatibility: In my application, I'm also using the GUI Loft visual editor to create my GUI and the Win32::GUI::SplashScreen module for a quick and easy splash screen. Both tools seem to work fine with this method of preforking a worker process. Actually, I think the splash screen is necessary because there is a noticable delay when calling fork() (or at least on my machine there is). See http://www.bahnhof.se/~johanl/perl/Loft/ for more info on GUI Loft. One final note on the "Attempt to free unreferenced scalar" errors. I've noticed that when I create my own Win32::GUI objects "by hand", I get "Attempt to free unreferenced scalar" errors on script termination. This is also noted in the examples from Win32::GUI::ThreadUtils. However, if I use Win32::GUI::Loft to create my GUI objects, I don't get any errors on script termination. I'm not sure why this is. I've included an example below. === My Configuration Details === Win32::GUI - 1.03 DBI - 1.53 DBD::mysql - 3.0002 perl - v5.8.4 built for MSWin32-x86-multi-thread (ActiveState binary build 810). The modules were installed from the ActiveState PPM2 Repository. MySQL server - 5.0 Well, I hope this can help someone. I'd love to hear any questions or comments. Thanks, Charles Alderman charlie-win32gui at aldermania dot com !; #### Example #### ## Obviously, this example is written for windows. I'm running XP. ## In this example, there is a GUI thread (parent) and a worker thread ## (child). The worker thread handles all DB connections to a MySQL ## database. ## ## Note: I just say thread, although each thread is actualy a ## pseudo-process that has been forked. The terminology may need ## some work. use strict; use warnings; use DBI; use Win32::GUI (); my $parent; # Indirect filehandle for the GUI thread sub main { my $win = Win32::GUI::Window->new( -name => 'MainWindow', -title => 'Forking Exmple', -pos => [100,100], -size => [300,300], ); $win->AddTextfield( -name => 'tfResults', -multiline => 1, -vscroll => 1, -autovscroll => 1, -pos => [10,10], -size => [280,200], ); $win->AddButton( -name => 'btQuery', -text => 'Run Query', -pos => [10, 220], -height => 25, ); ######################################################## ## Set up worker process... communicate through a pipe ## the worker thread eval's anything passed from the GUI thread. ## pipe my $child, $parent or die "Can't create pipe: $!"; my $pid = fork; die "Can't fork: $!" unless defined $pid; # Child... if ($pid == 0) { my $dbh = DBI->connect( 'dbi:mysql:test_db', 'user', 'pass' ); close $parent; while (<$child>) { print "$_\n"; my $results = eval $_; next unless $results; for my $row ( @$results ) { my $str = join ', ', @$row; $str .= "\r\n"; # \r\n necessary for the textfield $win->tfResults->Append( $str ); } } exit(0); } # Back to parent... # Make sure to set the pipe to auto-flush close $child; my $stdout = select $parent; $|++; select $stdout; ######################################################## $win->Show(); Win32::GUI::Dialog(); $win->DESTROY; } sub MainWindow_Terminate { close $parent; # This is necessary to clean up the worker thread wait; return(-1); } sub btQuery_Click { print $parent q{ $dbh->selectall_arrayref('SELECT * FROM my_table'); }; } # Start up... eval { main() }; Win32::GUI::MessageBox(0, "Error: $@", "DOH") if $@;