From: Johan L. <johanl@DarSerMan.com> - 2005-02-01 13:46:01
|
I experimented a bit with threads and how to use them to perform tasks in the background while still keeping the GUI responsive. This is a first draft. Hopefully it will be useful to someone. Demo with .gld file at: <http://www.darserman.com/Perl/Loft/temp/FetchURL-threads.zip> The program is used to fetch HTML from urls. The GET request is done in a separate thread. I tried various stuff but there was much segfaulting and variables lost between the threads. I ended up using worker queues to pass control and information between the threads. The program begins by starting a thread and two queues (one for passing urls to the getter thread, and one for returning the result. my $oThreadBrowserGet; my $oQueueBrowserGet; my $oQueueBrowserGetReturn; BEGIN { $oQueueBrowserGet = Thread::Queue->new; $oQueueBrowserGetReturn = Thread::Queue->new; $oThreadBrowserGet = threads->new( sub { while(my $url = $oQueueBrowserGet->dequeue()) { #... the GET is done here ... # $oQueueBrowserGetReturn->enqueue(Dumper($res)); } warn("Exiting getter thread\n"); }); } The thread is started in a begin block to have it occur before the Win32::GUI stuff is "use"d. I'm not sure, but I think that may be important. The thread basically waits for an URL to be put in the request queue. When one appears in the queue, the thread will GET the $url and put the serialized GET result object in the return value queue. If the passwd $url is undef, the while loop will terminate and the thread will end. When the thread is started, the GUI is loaded and the Dialog() main loop entered. This is what happens in the event handler that gets the url: sub ::btnFetch_Click { my ($win) = Win32::GUI::Loft::tglApp("winFetch") or return(1); my $url = $win->tfURL->Text(); $oQueueBrowserGet->enqueue($url); while( ! $oQueueBrowserGetReturn->pending() ) { Win32::GUI::DoEvents(); threads->yield(); } my $VAR1; my $res = eval( $oQueueBrowserGetReturn->dequeue() ); $@ and die($@); $win->tfHttp->Text($res->headers_as_string()); $win->tfHtml->Text($res->content()); } First the $url is put in the request queue. Then we wait for the return value to show up in the other queue. The result object is deserialized and used to populate the GUI controls (error checking omitted). For some reason Storable::freeze and thaw produced segfaults, whereas Data::Dumper works fine. Go figure. It would be trivial to continously pass progress info back to the GUI to update a ProgressBar control. I think most of this could be fairly neatly encapsulated in a module so there is a small amount of synchronization code in the application. /J -------- ------ ---- --- -- -- -- - - - - - Johan Lindström Sourcerer @ Boss Casinos johanl AT DarSerMan.com Latest bookmark: "TCP Connection Passing" http://tcpcp.sourceforge.net/ dmoz: /Computers/Programming/Languages/JavaScript/ 12 |
From: Johan L. <johanl@DarSerMan.com> - 2005-02-03 01:40:44
|
At 14:45 2005-02-01, Johan Lindstrom wrote: >I think most of this could be fairly neatly encapsulated in a module so >there is a small amount of synchronization code in the application. Yep, now I have a Task class (should probably be named Threads::Task or something) which encapsulates this. The demo app at: http://www.darserman.com/Perl/Loft/temp/FetchURL-threads.zip now supports a progress bar, and a cancel button to stop lengthy downloads mid way. This is (simplified) what it takes to set up a separate worker Task to fetch a web page: my $oTaskGet; BEGIN { $oTaskGet = Task->new( rsSub => sub { my ($oCaller, $oRequest) = @_; my $url = $oRequest; # ... start getting the $url #Report progress back to the main program $oCaller->response(Message::Progress->new(byteReceived => $byteReceived || 1)); #... finish getting the $url into a HTTP::Response object $oRes $oCaller->response($oRes); } ); $oTaskGet->addQueue("cancel get"); $oTaskGet->start(); } At the start() line, the new thread is spawned, waiting for a request() to be sent to it with an $url to fetch. When a request is received, the sub is called. During the lengthy download, progress information is sent back with different calls to the $oCaller->response() method. The response object can be anything, but in this case a message object is used to convey how many bytes has been received. The main program is responsible for making sense of these responses. The HTTP::Response from getting the $url is finally sent back in a response() and the sub returns, the thread going back to being idle, waiting for the next request. At the same time, in the bthFetch_Click event handler in the main program: $oTaskGet->request($url); while (1) { my $oResponse = $oTaskGet->waitResponse( sub { Win32::GUI::DoEvents() } ); if($oResponse->isa("Message::Begin")) { $win->pbDownload->SetRange(0, $oResponse->byteExpected || 1); } elsif($oResponse->isa("Message::Progress")) { $win->pbDownload->SetPos($oResponse->byteReceived); } elsif($oResponse->isa("HTTP::Response")) { my $resHttp = $oResponse; $win->tfHttp->Text($resHttp->headers_as_string()); last; } } First the request() is sent to the task thread. This is when the sub defined above kicks in and starts to fetch the web page. The call to waitResponse() blocks until there is any response object available, but in the meantime, it repeatedly calls the sub ref we provide. In Win32::GUI programs it's clever to call DoEvents() at this time. When the waitResponse() returns with a response object, we dispatch it depending on the class. If it's a Message::Progress, we update the ProgressBar control, etc. If it's the final HTTP::Response object, we're done so we exit the loop and leave the event handler. The Cancel button is handled in a similar way, but with the dedicated message queue ("cancel get") we set up before starting the thread. sub ::btnCancel_Click { $oTaskGet->request(Message::Cancel->new(), "cancel get"); } This puts a Message::Cancel object in that queue (which is different from the default request queue we sent the $url through). At the other end of the queue, in the loop fetching the web page chunks, we check to see if anything appears during the download. $oCaller->acceptQueue("cancel get") and die("cancel get\n"); In this case the queue is only used for this single purpose, so we don't need to check what kind of object it is. If anything is sent on this queue, it's time to cancel, and for LWP::UserAgent this is done by dying. That's all folks, /J -------- ------ ---- --- -- -- -- - - - - - Johan Lindström Sourcerer @ Boss Casinos johanl AT DarSerMan.com Latest bookmark: "TCP Connection Passing" http://tcpcp.sourceforge.net/ dmoz: /Computers/Programming/Languages/JavaScript/ 12 |