Anonymous Monk has asked for the wisdom of the Perl Monks concerning the following question:

Is there a way to execute a subroutine and return immediately without waiting for the subroutine to finish? I have a button that creates a report and puts the data into a widget in a separate frame. The report subroutine takes about 5 seconds to complete, which causes the button to stay 'sunken' and the entire MainWindow is unresponsive. I have searched CPAN, and Google, but I am probably not using the correct keyword, because I can't find anything.

Replies are listed 'Best First'.
Re: Executing functions in Perl::Tk
by liverpole (Monsignor) on Feb 24, 2010 at 00:49 UTC
    Yes, there are a couple of ways.

    The really simple approach is just to call the method update() on the main window object at appropriate times during the subroutine execution:

    # Assuming my $mw = MainWindow(...) sub button_press { # Do some work $mw->update; # Do some more work $mw->update; # etc...

    The more complex (but very powerful) approach is to use threads, as long as you're very careful not to do anything Tk-related within the thread.  (That's because Tk is not "thread-safe").

    Here's an example of a way you could use threads, especially if there are parts of the worker subroutine that have long delays during which you don't have the luxury of using $mw->update:

    #!/usr/bin/perl -w # # Libraries use strict; use warnings; use threads; use Tk; use Tk::Font; use Tk::ROText; # Main Program my $mw = new MainWindow(-title => 'Tk thread example'); my $fnt = $mw->Font(-family => 'tahoma', -size => '24'); my $btn = $mw->Button(-text => 'Press Me', -font => $fnt)->pack(-fill +=> 'x'); $btn->configure(-bg => '#ff7f3f', -command => \&button_press); my $txt = $mw->ROText(-bg => 'white')->pack(-fill => 'both'); $mw->bind('<Escape>' => sub { exit }); $mw->repeat(1000 => \&gui_loop); MainLoop; # Subroutines sub gui_loop { $txt->insert('end', sprintf "Time is %s\n", scalar localtime time( +)); $txt->see('end'); } sub button_press { my $sthread = threads->new(sub { worker_thread() }); $sthread->detach(); # Here we "return immediately" :) return; } sub worker_thread { my $tid = threads->tid; # But DON'T do this ... !!! # my $thread_mw = new MainWindow(); for (my $i = 0; $i < 1024; $i++) { printf "Thread %s: Doing more work ...\n", $tid; sleep 1; } }

    Please note that once you're in the worker thread, you must refrain from the urge to use Tk.  If you want to pass data back to the main program, you should look into shared data between threads.

    If you try putting back in the code where it says "But DON'T do this", you'll get an error message like:

    Thread 1: Doing more work ... Attempt to free non-existent shared string '_ErrorInfo_', Perl interpr +eter: 0x23 3f3c at C:/Perl/site/lib/Tk.pm line 423.

    s''(q.S:$/9=(T1';s;(..)(..);$..=substr+crypt($1,$2),2,3;eg;print$..$/
Re: Executing functions in Perl::Tk
by zentara (Cardinal) on Feb 24, 2010 at 12:55 UTC
    liverpole's thread example is pretty good, but for many reasons you really don't want to launch a thread for something as simple as a non-blocking execution of a function.

    For instance, the code in liverpole's example

    sub button_press { my $sthread = threads->new(sub { worker_thread() }); $sthread->detach(); # Here we "return immediately" :) return; }
    where the thread is created in a button callback, after Tk code has been invoked in main, has a possible problem. Many nodes have been posted previously, on how some of the "copy-on-write" Tk code gets copied into the thread, and sometimes causes "free to wrong pool" errors. The example works, but it is a very simple button press. I'm just saying, beware of the non-thread-safety of the Tk module. The general advice to absolutely avoid the possible problem, is shown in PerlTk on a thread....... the copy-on-write thread action combined with the non-thread safety of Tk, means you need to follow alot of restrictions for foolproof operation of your Tk-threaded program.

    However, threads aside, if you just want to execute something and move on, you can fork it off with a piped open, IPC::Open3, etc. Then use Tk::fileevent, or i used a timer in the first code block, to read the results as they come back in.

    #!/usr/bin/perl use warnings; use strict; use Tk; my $mw = MainWindow->new(-background => 'gray50'); my $text = $mw->Scrolled('Text')->pack(); my $pid; my $startb = $mw->Button( -text => 'Start', -command=> \&work, )->pack(); my $count = 0; my $label = $mw->Label(-textvariable=>\$count)->pack(); my $testtimer = $mw->repeat(500, sub { $count++} ); my $stopb = $mw->Button( -text => 'Exit', -command=>sub{ kill 9,$pid; exit; }, )->pack(); MainLoop; ##################################### sub work{ $startb->configure(-state=>'disabled'); use Fcntl; + my $flags; + #long 10 second delay between outputs $pid = open (my $fh, "top -b -d 10 |" ) or warn "$!\n"; fcntl($fh, F_SETFL, O_NONBLOCK) || die "$!\n"; # Set the non-block f +lags my $repeater; $repeater = $mw->repeat(10, sub { if(my $bytes = sysread( $fh, my $buf, 1024)){; $text->insert('end',$buf); $text->see('end'); } } ); }

    and reading with a fileevent

    #!/usr/bin/perl use warnings; use strict; use IPC::Open3; use Tk; $|=1; my $pid=open3(\*IN,\*OUT,0,'/bin/bash'); my $mw=new MainWindow; $mw->geometry("600x400"); my $t=$mw->Scrolled('Text',-width => 80, -height => 80, )->pack; &refresh; $mw->fileevent(\*OUT,'readable',\&write_t); my $id = Tk::After->new($mw,2000,'repeat',\&refresh); MainLoop; sub refresh{ print IN "top b n 1"; print IN "\n"; #absolutely needed and on separate line } sub write_t { my $str= <OUT>; $t->insert("1.0",$str); # $t->see("0.0"); } __END__

    I'm not really a human, but I play one on earth.
    Old Perl Programmer Haiku
Re: Executing functions in Perl::Tk
by doug (Pilgrim) on Feb 24, 2010 at 19:31 UTC

    This is more of a perception problem than a technical one. If you are more interested in hiding the issue than actually fixing it, launch the report generator in the background (system() with an & at the end) and have it write its output to a file. Then fire up a message box that says "report generation in progress" and has no clickable buttons. Next schedule a subroutine to run every 0.5 seconds to see if the report generation is done, and once it is, destroy the message box and slurp in the generated report.

    Although as ugly as sin, this doesn't require any fancy threading, and doesn't block the application from redrawing the windows. And that annoying message box (and you know it will annoy your users) will keep them busy/pacified for the 5 seconds or so you said that are needed to generate the report. You can put a countdown or progress bar on that message if you want to get fancy and can really estimate when it will be done.

    - doug