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

Despite reading previous examples of questions involving Tk and threads, I still can't work out the following. I have a script that runs various other programs and collates the results. It requires a large configuration file, and since users don't like to edit this I've written a Tk front end that allows them to set up the configuration. It then writes out a config file and runs original script. The users need to see the output of the script, so I have the front end run something like this:

sub runprogs { # run the main script my $pipe = IO::Pipe->new(); $pipe->reader("$original_script 2>&1"); # create display window my $top = $mw-> Toplevel(); my $label = $top -> Label(-text=>"STDOUT from script",-relief=>"groo +ve")->pack(); my $ro = $top->Scrolled('ROText', -width => 60, -height=>20, -scrollb +ars=>"e")->pack(); # continuously display output while (<$pipe>) { $ro->insert("end", "$_"); $ro->update(); sleep 1; } # add close button my $close = $top -> Button(-text=>"Close window", -command=>sub { destroy $top; })->pack(); }

The effect of this is that both the main window $mw and $top won't update whilst the while loop is running. The program can take some time to run, so users may minimise the windows and find them blank upon viewing them again before this loop has finished.

I cannot, of course, simply run the code above as a thread and detach it, as it makes reference to an existing Tk object. Can anyone suggest a better way to get around this?

Replies are listed 'Best First'.
Re: Tk + threads
by BrowserUk (Patriarch) on Jan 10, 2006 at 15:13 UTC

    Play with this. Example usage at the __END__. You'll need a separate Thread::Queue for each thread.

    #! perl -slw use strict; use threads; use Thread::Queue; use IO::Pipe; sub runNGather { my( $Q, $cmd ) = @_; my $pipe = IO::Pipe->new(); $pipe->reader( "$cmd 2>&1" ); $Q->enqueue( $_ ) while <$pipe>; $Q->enqueue( undef ); return; } my $Q = new Thread::Queue; threads->create( \&runNGather, $Q, $ARGV[0] )->detach; require Tk; require Tk::ROText; my $top = MainWindow->new; #$mw-> Toplevel(); my $label = $top -> Label( -text=>"STDOUT from script", -relief=>"groove" )->pack(); my $ro = $top->Scrolled( 'ROText', -width => 60, -height=>20, -scrollbars=>"e" )->pack(); # continuously display output while( $_ = $Q->dequeue ) { $ro->insert("end", "$_"); $ro->update(); sleep 1; } # add close button my $close = $top->Button( -text=>"Close window", -command=>sub { destroy $top; } )->pack(); __END__ P:\test>522177 "ping www.perlmonks.org"

    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    Lingua non convalesco, consenesco et abolesco. -- Rule 1 has a caveat! -- Who broke the cabal?
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
      Thanks. When running this, I get the same effect (blanked windows) unless I remove the sleep command whilst dequeueing. If so, then ping's output is frequent enough for the window to be updated.

        I just knocked up something to demonstrate the technique, I didn't do any debugging. For your purposes you need to handle multiple queues simultaneously, so you would need to use dequeue_nb() (non-blocking) in the while loop. You also need to coordinate the queues with their associated windows, so the logic required is slightly more complex.

        This version, equally minimally tested, will run multiple concurrent commands as shown at the __END__. I've used Win32::Sleep to allow the updates to happen quickly whilst not running away with the cpu. You could use select, or Time::HiRes or yield() (though the latter tends to thrash the cpu!).

        #! perl -slw use strict; use threads; use Thread::Queue; use IO::Pipe; sub runNGather { my( $Q, $cmd ) = @_; my $pipe = IO::Pipe->new(); $pipe->reader( "$cmd 2>&1" ); $Q->enqueue( $_ ) while <$pipe>; $Q->enqueue( '!!!EOF!!!' ); return; } my @Qs; for( @ARGV ) { push @Qs, [ new Thread::Queue ]; threads->create( \&runNGather, $Qs[-1][0], $_ )->detach; } require Tk; require Tk::ROText; my $mw = MainWindow->new; for my $Q ( @Qs ) { my $top = $mw->Toplevel(); my $label = $top -> Label( -text=>"STDOUT from script", -relief=>"groove" )->pack(); my $ro = $top->Scrolled( 'ROText', -width => 60, -height=>20, -scrollbars=>"e" )->pack(); # add close button my $close = $top->Button( -text => "Close window", -command => sub{ destroy $top; } )->pack(); push @{ $Q }, $ro; } # continuously display output while( 1 ) { last unless @Qs; for my $n ( 0 .. $#Qs ) { my $text = $Qs[ $n ][0]->dequeue_nb; next unless defined $text; if( $text =~ m[^!!!EOF!!!$] ) { my $Q = splice( @Qs, $n, 1 ); undef $Q->[0]; last; } my $ro = $Qs[ $n ][1]; $ro->insert("end", "$text"); $ro->update(); } $mw->update; Win32::Sleep .1; } $mw->MainLoop; __END__ P:\test>522177 "ping www.perlmonks.org" "ping www.bbc.co.uk" "dir /s \ +*.pl"

        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        Lingua non convalesco, consenesco et abolesco. -- Rule 1 has a caveat! -- Who broke the cabal?
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.
Re: Tk + threads
by zentara (Cardinal) on Jan 10, 2006 at 17:07 UTC
    I see a couple of problems.

    The "while(<$pipe>)" is not good in gui programming. You should look at Tk::fileevent, and try to design using it. It allows you to watch filehandles in a non-blocking manner.

    When using Tk and threads, it is best to use Tk only in your main thread, to display what is going on in the worker threads. In the worker thread, don't access the Tk display widgets, rather pass the data back to the main thread thru a Tk timer and shared variables.

    #!/usr/bin/perl use strict; use Tk; use threads; use threads::shared; my $data_out:shared = ''; my $data_in:shared = ''; my $thread_die = 0; #create the thread before you start any Tk so the #thread dosn't get any Tk stuff copied in my $wthr = threads->new( \&update_thread )->detach; create_tk_window(); exit; ######################################################### #the non-Tk worker code sub update_thread { print "update_thread called...\n"; while (1) { if($thread_die == 1){return} $data_in = 'thread-processing'.$data_out; sleep 1; } END: } ######################################################### sub create_tk_window { my $mw = MainWindow->new( -background => 'black', -foreground => 'yellow', -title => "Thread Test" ); $mw->geometry("600x400"); $mw->minsize( 400, 400 ); $mw->maxsize( 800, 800 ); my $sent_text = $mw->Scrolled('Text', -height => 10, -width => 60, -background => 'black', -foreground => 'yellow' )->pack( -side => 'bottom', -anchor => 's', -pady => 2 ); my $received_text = $mw->Scrolled( "Text", -height => 10, -width => 60, -background => 'white', -foreground => 'black', )->pack(); my $repeater; $mw->Button(-text=> 'Exit', -command => sub{ $thread_die = 1; $repeater->cancel; $mw->withdraw; kill 9, $$; })->pack; $repeater = $mw->repeat(1000, sub{ $data_out++; $sent_text->insert( 'end', "Sent $data_out\n" ); $sent_text->see('end'); $received_text->insert( 'end', "Received $data_in\n" ); $received_text->see('end'); }); MainLoop; }

    I'm not really a human, but I play one on earth. flash japh
Re: Tk + threads
by markwx (Acolyte) on Jan 10, 2006 at 15:00 UTC
    This may not be helpful as my Tk experience is V. limited - but I would.
    1. Have code to create TK objects
    2. Create a Thread::Queue and a threads::shared variable to check if process is finished.
    3. Create 'thread' passing in my Queue as param
    4. Within thread, launch separate prog. Write prog's ouput to Queue. Set the threads::shared variable when complete.
    5. In main body, loop and check for output on Queue with dequeue_nb. If I get some, write it to Tk objects. Check threads::shared variable to see if process is finished. When process is finished exit loop.
    6. Join threads.

      Thanks for your suggestions. I've knocked up something along those lines, which works until the end of the script at which point I see a barrage of errors on STDOUT, e.g.

      Unbalanced string table refcount: (2) for "VERSION" during global dest +ruction. Scalars leaked: -3118

      Do you have any idea of what this might be?