Beefy Boxes and Bandwidth Generously Provided by pair Networks
P is for Practical
 
PerlMonks  

Tk and Threads

by Dirk80 (Pilgrim)
on Dec 21, 2010 at 13:58 UTC ( [id://878256]=perlquestion: print w/replies, xml ) Need Help??

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

Hello,

I have written a small example how I use threads with TK at the moment.

It is working, but it is polluting my design and code. So I'm sure that there has to be a better solution than mine.

First I want to present you my solution:

thread_example.pl:

#!/usr/bin/perl ########################################################### # USES ########################################################### use strict; use warnings; use Tk; use My::Gui; use My::ThreadManager; ########################################################### # MAIN FUNCTION ########################################################### &createMyThread(); &createMyGui(); &MainLoop();

My/Globals.pm:

package My::Globals; ########################################################### # USES ########################################################### use strict; use warnings; use base 'Exporter'; use constant { FALSE => 0, TRUE => 1, CANCEL => 2 }; ########################################################### # EXPORT ########################################################### our @EXPORT = qw(TRUE FALSE CANCEL); ########################################################### # VARIABLES ########################################################### # NONE ########################################################### # PUBLIC FUNCTIONS ########################################################### # NONE ########################################################### # PRIVATE FUNCTIONS ########################################################### # NONE 1;

My/Gui.pm:

package My::Gui; ########################################################### # USES ########################################################### use strict; use warnings; use base 'Exporter'; use Tk; use My::ThreadManager; ########################################################### # EXPORT ########################################################### our @EXPORT = qw(createMyGui); ########################################################### # VARIABLES ########################################################### my %gui; my %thread_data = ( 'number' => 7 ); ########################################################### # PUBLIC FUNCTIONS ########################################################### sub createMyGui { $gui{'mw'} = new MainWindow; $gui{'mw'}->title("Thread Example"); $gui{'mw'}->protocol('WM_DELETE_WINDOW' => sub { evWorkCancel() unless isWorkFinished(); killMyThread(); }); fillMainWindow($gui{'mw'}); } ########################################################### # PRIVATE FUNCTIONS ########################################################### sub fillMainWindow { my $mw = $_[0]; $mw->{'start_b'} = $mw->Button('-relief' => 'raised', '-text' => 'START', '-command' => sub { evWorkStart(\%thread_data); }); $mw->{'cancel_b'} = $mw->Button('-relief' => 'raised', '-text' => 'CANCEL', '-command' => sub { evWorkCancel(); }); $mw->{'start_b'}->pack(); $mw->{'cancel_b'}->pack(); } 1;

My/ThreadManager.pm:

package My::ThreadManager; ########################################################### # USES ########################################################### use strict; use warnings; use base 'Exporter'; use Data::Dumper; use threads; use threads::shared; use Thread::Queue; use My::Globals; use My::ThreadWorker; use feature qw(say); use constant { EV_NONE => 0, EV_KILL => 1, EV_WORK_START => 2, EV_WORK_CANCEL => 3, EV_WORK_FINISH => 4 }; use constant { STATE_IDLE => 0, STATE_WORK => 1 }; ########################################################### # EXPORT ########################################################### our @EXPORT = qw(createMyThread killMyThread evWorkStart evWorkCancel isWorkFinished shallWorkBeCancelled ); ########################################################### # VARIABLES ########################################################### my $thread; my $thread_event:shared = EV_NONE; my $thread_state:shared = STATE_IDLE; my $q = Thread::Queue->new(); ########################################################### # PUBLIC FUNCTIONS ########################################################### sub createMyThread { $thread = threads->create( \&execMyThread ); } sub execMyThread { STATE_IDLE: $thread_state = STATE_IDLE; say "STATE_IDLE"; while(1) { if( $thread_event == EV_WORK_START ) { say "EV_WORK_START"; $thread_event = EV_NONE; goto STATE_WORK; } elsif( $thread_event == EV_KILL ) { say "EV_KILL"; $thread_event = EV_NONE; return; } else { # wait select(undef,undef,undef,0.1); } } STATE_WORK: $thread_state = STATE_WORK; say "STATE_WORK"; while(1) { if( ($thread_event == EV_WORK_FINISH) || ($thread_event == EV_WORK_CANCEL) ) { say "EV_WORK_FINISH" if( $thread_event == EV_WORK_FINISH ) +; say "EV_WORK_CANCEL" if( $thread_event == EV_WORK_CANCEL ) +; $thread_event = EV_NONE; goto STATE_IDLE; } elsif( $thread_event == EV_KILL ) { say "EV_KILL"; $thread_event = EV_NONE; return; } else { my %thread_data; select(undef,undef,undef,0.1); while( my $thread_data_str1 = $q->dequeue ) { %thread_data = %{ eval $thread_data_str1 }; }; if( &work(\%thread_data) == CANCEL ) { $thread_event = EV_WORK_CANCEL; } else { $thread_event = EV_WORK_FINISH; } } } } sub killMyThread { $thread_event = EV_KILL; $thread->join; exit 0; } sub evWorkStart { my $ref_thread_data = $_[0]; my $thread_data_str1 = ""; $Data::Dumper::Varname = "thread_data_str"; $thread_data_str1 = Dumper($ref_thread_data); $q->enqueue( $thread_data_str1 ); $q->enqueue( undef ); $thread_event = EV_WORK_START; while( $thread_state != STATE_WORK ) { select(undef, undef, undef, 0.1); } } sub evWorkCancel { $thread_event = EV_WORK_CANCEL; while( $thread_state != STATE_IDLE ) { select(undef, undef, undef, 0.1); } } sub isWorkFinished { if( $thread_state == STATE_IDLE ) { return TRUE; } else { return FALSE; } } sub shallWorkBeCancelled { if( $thread_event == EV_WORK_CANCEL ) { return TRUE; } else { return FALSE; } } ########################################################### # PRIVATE FUNCTIONS ########################################################### # NONE 1;

My/ThreadWorker:

package My::ThreadWorker; ########################################################### # USES ########################################################### use strict; use warnings; use base 'Exporter'; use My::Globals; use My::ThreadManager; use Data::Dumper; use feature qw(say); ########################################################### # EXPORT ########################################################### our @EXPORT = qw( work ); ########################################################### # VARIABLES ########################################################### # NONE ########################################################### # PUBLIC FUNCTIONS ########################################################### sub work { my ($ref_thread_data) = @_; while( $ref_thread_data->{'number'} <= 100 ) { print $ref_thread_data->{'number'} . " "; select(undef, undef, undef, 0.1); $ref_thread_data->{'number'} += 1; # NOTE: # Bad design because My::ThreadWorker needs # My::ThreadManager and My::ThreadManager # needs MyThreadWorker if( My::ThreadManager::shallWorkBeCancelled() == TRUE ) { print "\n"; return CANCEL; } } print "\n"; }

In reality the work function of package "My::ThreadWorker" is a complex function which is calling a lot of other modules. I want to be able to cancel this work function to every time and NOT only when it is calling My::ThreadManager::shallWorkBeCancelled.

I do not like to make the call to this function. Because I have to enter this call to a lot of code. In my opinion the code is polluted.

Is there a solution to return from the work function when the user is pressing the cancel-button without polluting the code with My::ThreadManager::shallWorkBeCancelled?

And the other problem is the design. "ThreadWorker" needs "ThreadManager" and "ThreadManager" needs "ThreadWorker".

Thank you alot for your help

Greetings,

Dirk

Replies are listed 'Best First'.
Re: Tk and Threads
by zentara (Archbishop) on Dec 21, 2010 at 17:39 UTC
    From what I can see from the code, you are launching threads from a Tk callback
    $mw->{'start_b'} = $mw->Button('-relief' => 'raised', '-text' => 'START', '-command' => sub { evWorkStart(\%thread_data); });

    I am surprised that you are not having thread crashes due to Tk's non-threadsafety. The code might work good if converted to Gtk2, which has better thread safety.

    Back to your problem, I did something awhile back, that showed how to reuse threads with Tk... see Tk-with-worker-threads. Some of the improvements in perl thread signal handling, have since made my nested loops unnessary, thru thread signal handling. See using the thread->kill() feature on linux

    But the main idea to make non-polluting code, is to recycle and reuse of course, so see Tk-with-worker-threads

    Just beware, that Tk is NOT threadsafe, and unless you follow the many precautions, listed here, by various monks, you will have problems. See Re: Perl Tk and Threads


    I'm not really a human, but I play one on earth.
    Old Perl Programmer Haiku ................... flash japh

      Thank you for your answer. But I think I have to clarify a bit to avoid misunderstandings.

      No I'm not starting the thread from a Tk callback. I know that this causes problems. The thread is started at the beginning in "thread_example.pl" with the call createMyThread().

      The thread is running before Tk code is executed. I gave the thread two states: STATE_IDLE and STATE_WORK. My idea was to change the states via Events. Initially the thread is in the STATE_IDLE. So it is nothing doing as waiting.

      Within the Tk callback I'm calling evWorkStart. This function is generating the event EV_WORK_START and giving data to the thread via a queue. So the state of the thread is changed from STATE_IDLE to STATE_WORK. In STATE_WORK the work function of the "ThreadWorker" is called.

      When the work is done the thread is automatically changing its state to STATE_IDLE.

      But the user has the possibility to press the cancel-button before the work is finished. This calls the function evWorkCancel and the event EV_WORK_CANCEL is generated. This event causes the thread to go back to the STATE_IDLE.

      The problem is now that the work function is in reality very complex and has a lot of code in many modules. To achieve the goal that this work function is returning very fast I inserted function calls shallWorkBeCancelled which check if the event EV_WORK_CANCEL is there, i.e. the user pressed the cancel-button.

      Because the work function is complex I had to call shallWorkBeCancelled in several modules which are called from the work-function. This is in my opinion pollution of the code.

      I read the article about Tk-with-Worker-Threads. But if I understand it right it is more or less doing the same than me. It uses the shared variable thread_die.

      The work-function in the Tk-with-worker-threads is very simple. So you only had to add the line if( $thread_die == 1 ){return} at one place. But if you also would have a complex function there you would have to place this line at several places. And so there would be the same pollution of code.

      Please correct me If I am saying something wrong.

      Your hint with the signals sound interesting. But I don't know it until now. I first have to learn about and try. And for me it is important that it is working with linux and windows. If signals only work with linux then this solution will not help me unfortunately.

      Switching to gtk is also a good hint. But the problem is that I have a lot of GUI-stuff written in Tk. So if possible I want to realize the threading with Tk. But I really think about learning GTK to develop my new code with this gui-environment.

      Or perhaps I'm thinking to complex. Do I really need a thread? My scenario is that the user is pressing on a start button. This shall start a function with a lot of work. I want that the user can interrupt this function at any time by pressing the cancel-button.

      And I really want to say explicitly thank you to you zentara. Because nearly all my knowledge about "threads and tk" I have from posts of you which I read here in the perlmonks forum. Only with this help I was able to develop my "tk and thread solution".

      Greetings,

      Dirk

        I read the article about Tk-with-Worker-Threads. But if I understand it right it is more or less doing the same than me. It uses the shared variable thread_die. The work-function in the Tk-with-worker-threads is very simple. So you only had to add the line if( $thread_die == 1 ){return} at one place. But if you also would have a complex function there you would have to place this line at several places. And so there would be the same pollution of code. Please correct me If I am saying something wrong.

        No, you are right on the money, and I just missed the subtle interactions in your code. I'm use to monolithic scripts. ;-)

        Your hint with the signals sound interesting. ..........Or perhaps I'm thinking to complex. Do I really need a thread? My scenario is that the user is pressing on a start button. This shall start a function with a lot of work. I want that the user can interrupt this function at any time by pressing the cancel-button

        You are right, on the same analytic path, that everyone else has followed.

        It took quite awhile to get the thread->kill signal working in the threads module. But even it, has a drawback. The docs state that a thread kill signal will not interrupt some io operations, so if you have a socket open, or some disk write hang, the thread will still not respond to the kill signal.

        So it seems you have to be very careful about wrapping things in eval statements, etc.

        There is also another problem with reusable threads, is that the memory used remains claimed by Perl, so the mem usage of a thread will be the peak mem of runs.

        There is a comprimise. It helps remove polluting code and fixes the memory use problem. Fork off the heavy work code in the threads, in a way that lets you get a pid of the process. Then stuff that pid in a thread local variable. Now, when you send a thread->kill signal to your thread,(or maybe STATE_DIE instead of signals?) in the signal callback, you can do a killfam($pid). You must be aware of the parent pid and all the children spawned, so you need killfam instead of plain kill. Read the signal(7) manpage. You can create your own custom signal.

        Or you could stick with your event_state method, and setup timers in your thread, to check for your states( as in my $thread_die variable)

        Moving to Gtk2 would be a good move, because the event looping system is much better. In Gtk2, each thread can have it's own GLib event loop.

        Imagine, a master loop, controlling many loops in other threads.

        I really want to say explicitly thank you

        Hey, I was hoping someone would come up with an object layer abstraction on top of threads, and you are doing it. Thank YOU. :-)

        But, remember one thing. Threads are only really useful when you need realtime sharing of data BETWEEN threads. If you are interested in just controlling threads, who happily run in their own space, you are better off forking, just to avoid memory gain problems, and possible non-threadsafe modules.

        Finally, from the threads perldoc:

        Correspondingly, sending a signal to a thread does not disrupt the operation the thread is currently working on: The signal will be acted upon after the current operation has completed. For instance, if the thread is stuck on an I/O call, sending it a signal will not cause the I/O call to be interrupted such that the signal is acted up immediately.

        So you cannot throw ANY code into a thread codeblock, and expect signals to work.


        I'm not really a human, but I play one on earth.
        Old Perl Programmer Haiku ................... flash japh
Re: Tk and Threads
by BrowserUk (Patriarch) on Dec 22, 2010 at 00:09 UTC

    Here is a small sample (taken almost verbatim from the threads POD), demonstrating cancelling threads from a Tk gui:

    #!perl -slw use strict; use Time::HiRes qw [ sleep ]; use threads; use Thread::Queue; sub work{ my( $id, $delay, $Q ) = @_; my $n = 0; local $SIG{QUIT} = sub{ threads->exit; }; while( sleep( $delay ) && ++$n <= 100 ) { $Q->enqueue( "$id:$n" ); } } my $Q = new Thread::Queue; my @threads = map threads->new( \&work, $_, 0.1 * $_, $Q ), 1 .. 2; require Tk::ProgressBar; my $mw = MainWindow->new; my $pb1 = $mw->ProgressBar()->pack(); my $bt1 = $mw->Button( -text => 'Cancel thread 1', -command => sub { $threads[0]->kill('QUIT') } )->pack; my $pb2 = $mw->ProgressBar()->pack(); my $bt2 = $mw->Button( -text => 'Cancel thread 2', -command => sub { $threads[1]->kill('QUIT') } )->pack; my $repeat; $repeat = $mw->repeat( 100 => sub { while( $Q->pending ) { my( $id, $progress ) = split ':', $Q->dequeue; return unless $progress; ( $id == 1 ? $pb1 : $pb2 )->value( $progress ); if( $id == 2 && $progress == 100 ) { $repeat->cancel; $mw->exit; } } } ); $mw->MainLoop; END{ $_->join for @threads; }

    Wrapping that over in a bunch of unnecessary verbose complexity is left as an exercise for the reader :)


    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.

      Thank you for your reply.

      This is a good example for thread signalling. But one problem remains. You start your threads before Tk Code. And you are able to cancel your threads. But how could you restart your threads? If I am right this should not be done from Tk.

      I tried the signalling with my example with the states in the thread. My goal is it to change the state from STATE_WORK to STATE_IDLE when the signal comes. But always when the signal comes the thread is completely killed.

        With a minor change to the code I posted, the button can be used to pause & resume the threads:

        #!perl -slw use strict; use Time::HiRes qw [ sleep ]; use threads; use Thread::Queue; use constant { IDLE => 0, WORK => 1 }; sub work{ my( $id, $delay, $Q ) = @_; my $n = 0; my $state = WORK; local $SIG{QUIT} = sub{ $state ^= 1 }; while( sleep( $delay ) && ++$n <= 100 ) { sleep 1 while $state == IDLE; $Q->enqueue( "$id:$n" ); } } my $Q = new Thread::Queue; my @threads = map threads->new( \&work, $_, 0.1 * $_, $Q ), 1 .. 2; require Tk::ProgressBar; my $mw = MainWindow->new; my $pb1 = $mw->ProgressBar()->pack(); my $bt1 = $mw->Button( -text => 'Pause/resume thread 1', -command => sub { $threads[0]->kill('QUIT') } )->pack; my $pb2 = $mw->ProgressBar()->pack(); my $bt2 = $mw->Button( -text => 'Pause/resume thread 2', -command => sub { $threads[1]->kill('QUIT') } )->pack; my $repeat; $repeat = $mw->repeat( 100 => sub { while( $Q->pending ) { my( $id, $progress ) = split ':', $Q->dequeue; return unless $progress; ( $id == 1 ? $pb1 : $pb2 )->value( $progress ); if( $id == 2 && $progress == 100 ) { $repeat->cancel; $mw->exit; } } } ); $mw->MainLoop; END{ $_->join for @threads; }

        But that is actually easier to achieve using a simple shared variable.


        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.
Re: Tk and Threads
by roboticus (Chancellor) on Dec 21, 2010 at 15:28 UTC

    Dirk80:

    Update: Never mind. You explicitly stated that your ThreadWorker was a simplified version of the real one, so this comment doesn't necessarily apply.


    I'm only commenting on this bit of your post:

    And the other problem is the design. "ThreadWorker" needs "ThreadManager" and "ThreadManager" needs "ThreadWorker".

    I don't see that a circular dependency is necessarily a problem. If it were me, I'd just put the ThreadWorker package in the same source module as ThreadManager, for two reasons:

    1. It's a small class, and would get a bit smaller when combined with ThreadWorker (it would share some use declarations, some block comments, and such.
    2. It's not directly exposed to the application. (It only contains the work function--you don't even need to export it, as you could directly call it.)

    ...roboticus

    When your only tool is a hammer, all problems look like your thumb.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://878256]
Approved by ww
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others avoiding work at the Monastery: (4)
As of 2024-03-29 06:03 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found