Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perl threading of object method

Perl rookie here, so please be gentle :)

I've written following code to keep track of my dogs when I'm hunting (not really). Every time a dog finds a duck, it signals the main thread, which then collects information from each of the dogs in the pack.

#!/usr/bin/env perl

use strict;
use warnings;
use v5.14;

use threads;

{
    package Dog;

    sub new {
        my ($class, $name, $dt) = @_;
        my $self = {
            dt => $dt,      # will find a duck every $dt seconds
            name => $name,
            ducksfound => 0
        };
        bless $self, $class;
    }

    sub hunt {
        #
        # the "thread" method -- the dog will hang around for $dt seconds,
        # then alert the main thread by sending SIGUSR1
        #
        my $self = shift;
        while (1) {
            sleep $self->{dt};
            $self->{ducksfound} += 1;
            kill USR1 => $$;
        }
    }

    sub bark {
        my $self = shift;
        sprintf "%s: found %d ducks!", ($self->{name}, $self->{ducksfound});
    }

    1;
}

my @dogs;

$SIG{USR1} = sub {
    say join ", ", map { $_->bark } @dogs;
};


push @dogs, Dog->new("Labrador", 1);
push @dogs, Dog->new("Retriever", 2);
push @dogs, Dog->new("Shepherd", 3);

threads->create( sub { $_->hunt } ) for @dogs;
$_->join for threads->list;

Expected output of the above code would be something like:

Labrador: found 1 ducks!, Retriever: found 0 ducks!, Shepherd: found 0 ducks!

Labrador: found 2 ducks!, Retriever: found 0 ducks!, Shepherd: found 0 ducks!

Labrador: found 3 ducks!, Retriever: found 0 ducks!, Shepherd: found 0 ducks!

Labrador: found 3 ducks!, Retriever: found 1 ducks!, Shepherd: found 0 ducks!

Labrador: found 4 ducks!, Retriever: found 1 ducks!, Shepherd: found 0 ducks!

Labrador: found 5 ducks!, Retriever: found 1 ducks!, Shepherd: found 0 ducks!

Labrador: found 6 ducks!, Retriever: found 1 ducks!, Shepherd: found 0 ducks!

Labrador: found 6 ducks!, Retriever: found 1 ducks!, Shepherd: found 0 ducks!

Labrador: found 6 ducks!, Retriever: found 1 ducks!, Shepherd: found 1 ducks!

Instead, what I get is the following:

Labrador: found 1 ducks!, Retriever: found 0 ducks!, Shepherd: found 0 ducks!

Labrador: found 2 ducks!, Retriever: found 0 ducks!, Shepherd: found 0 ducks!

Labrador: found 3 ducks!, Retriever: found 0 ducks!, Shepherd: found 0 ducks!

Labrador: found 0 ducks!, Retriever: found 1 ducks!, Shepherd: found 0 ducks!

Labrador: found 4 ducks!, Retriever: found 0 ducks!, Shepherd: found 0 ducks!

Labrador: found 5 ducks!, Retriever: found 0 ducks!, Shepherd: found 0 ducks!

Labrador: found 0 ducks!, Retriever: found 2 ducks!, Shepherd: found 0 ducks!

Labrador: found 0 ducks!, Retriever: found 0 ducks!, Shepherd: found 1 ducks!

Notice how every dog's duck count resets to zero whan another dog is speaking.

Any insights as to which particular footnote I must have glossed over while reading the Llama?

like image 689
K-spacer Avatar asked Feb 14 '13 14:02

K-spacer


1 Answers

The fundamental problem is that Perl variables are not shared by default, which combines with a bit of weirdness about which thread is servicing which signal to produce the result you're seeing.

When you spawn your hunting threads, each of them gets its own copy of @dogs and its contents. That's just the way Perl threads work: the interpreter and its current state -- @dogs, %SIG, the open STDOUT -- is cloned entire. To see how that works, consider this code:

my %dog_decls = (
    Labrador    => 1,
    Retriever   => 2,
    Shepherd    => 3,
);

while (my ($name, $delay) = each %dog_decls) {
    my $dog = Dog->new($name, $delay);
    push @dogs, $dog;
    threads->create(sub { $dog->hunt });
}

$_->join for threads->list;

The cloning happens at threads->create time, so each of these threads is getting a different version of @dogs to take with it. As a consequence, the list of Dogs that bark when one of them catches a duck depends on which thread catches the signal! (Also note that you can infer the order in which each happened to emit the hash from this output.)

Retriever: found 0 ducks!, Labrador: found 1 ducks!

Retriever: found 0 ducks!, Labrador: found 2 ducks!

Retriever: found 1 ducks!

Retriever: found 0 ducks!, Labrador: found 3 ducks!

Retriever: found 0 ducks!, Labrador: found 4 ducks!

Retriever: found 0 ducks!, Labrador: found 0 ducks!, Shepherd: found 1 ducks!

Back to your code: When the Labrador thread (thread 1) wakes up, it updates the Labrador's ducksfound and sends a SIGUSR1. Someone (and we'll talk more about who in a second) sees the signal and barks all the Dogs. But the only Labrador that's been changed is the one in thread 1. The Retriever and Shepherd threads (threads 2 and 3 respectively) have not seen the update to Labrador's ducksfound.

Why then is the value for ducksfound printed correctly at first? Because of the way you installed the signal handler. You installed it process-wide -- recall that I said %SIG was among the things cloned to your threads. So each of the threads has a handler for USR1 that causes all the Dogs to bark. When you send USR1 to $$, whichever thread happens to be awake at that moment catches it. And it so happens that the thread that sent the signal is the thread that is awake.

And that explains why when the Retriever catches its first duck, its ducksfound value is correct but the Labrador's is not. Retriever catches the duck in thread 2, which sends SIGUSR1 to itself and then barks all its Dogs. But in thread 2, the Labrador has never been updated, and so the bark shows 0 for Labrador and 1 for Retriever.

The problem of unshared variables can be gotten around fairly simply by use of threads::shared:

use threads::shared;
...
my @dogs :shared;
...
push @dogs, shared_clone(Dog->new("Labrador",  1));

Now when one thread updates a Dog, all threads will see it and so it doesn't matter which thread is servicing the signal. Which is good, because in your code the "main thread" (thread 0) never gets back control. This might be okay, but probably leads to slightly weirder behavior than you expect.

If you actually want there to exist a manager thread, you probably need to spawn it explicitly:

# in Dog::new
        my ($class, $name, $hunter, $dt) = @_;
        ...
        hunter => $hunter,
# in Dog::hunt
        $self->{hunter}->kill('USR1');
# in main
my $hunter_thread = threads->create(
    sub {
        local $SIG{USR1} = sub {
            say join ", ", map { $_->bark } @dogs;
        };
        while (1) { usleep 100_000 } # higher resolution than hunt events
    }
);
...
push @dogs, shared_clone(Dog->new("Labrador", $hunter_thread, 1));

Note that just putting in a manager thread without sharing your Dogs would result in a thread that wakes up to print a bunch of zeroes. You need to do both to get the results you were expecting.

like image 145
darch Avatar answered Oct 08 '22 09:10

darch