8

这里是 Perl 菜鸟,所以请温柔:)

我编写了以下代码来在我打猎时跟踪我的狗(不是真的)。每次狗找到鸭子时,它都会向主线程发出信号,然后从包中的每只狗那里收集信息。

#!/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;

上述代码的预期输出将类似于:

拉布拉多:找到 1 只鸭子!,猎犬:找到 0 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 2 只鸭子!,猎犬:找到 0 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 3 只鸭子!,猎犬:找到 0 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 3 只鸭子!,猎犬:找到 1 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 4 只鸭子!,猎犬:找到 1 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 5 只鸭子!,猎犬:找到 1 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 6 只鸭子!,猎犬:找到 1 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 6 只鸭子!,猎犬:找到 1 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 6 只鸭子!,猎犬:找到 1 只鸭子!,牧羊犬:找到 1 只鸭子!

相反,我得到的是以下内容:

拉布拉多:找到 1 只鸭子!,猎犬:找到 0 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 2 只鸭子!,猎犬:找到 0 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 3 只鸭子!,猎犬:找到 0 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 0 只鸭子!,猎犬:找到 1 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 4 只鸭子!,猎犬:找到 0 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 5 只鸭子!,猎犬:找到 0 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:找到 0 只鸭子!,猎犬:找到 2 只鸭子!,牧羊犬:找到 0 只鸭子!

拉布拉多:发现 0 只鸭子!,猎犬:发现 0 只鸭子!,牧羊犬:发现 1 只鸭子!

请注意,当另一只狗说话时,每只狗的鸭子数量如何重置为零。

关于我在阅读 Llama 时必须忽略的特定脚注的任何见解?

4

1 回答 1

8

根本问题是 Perl 变量在默认情况下不共享,这与关于哪个线程正在服务哪个信号以产生您所看到的结果的一些奇怪结合在一起。

当您生成狩猎线程时,每个线程都会获得自己的副本@dogs及其内容。这正是 Perl 线程的工作方式:解释器和它的当前状态—— @dogs, %SIG, STDOUTopen——被整个克隆。要了解它是如何工作的,请考虑以下代码:

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;

克隆有时会发生threads->create,因此这些线程中的每一个都可以使用不同的版本@dogs。因此,Dogs当其中一个捕捉到鸭子时,该吠声的列表取决于哪个线程捕捉到信号!(另请注意,您可以each从该输出推断出恰好发出散列的顺序。)

猎犬:找到 0 只鸭子!,拉布拉多:找到 1 只鸭子!

猎犬:找到 0 只鸭子!,拉布拉多:找到 2 只鸭子!

寻回犬:发现 1 只鸭子!

猎犬:找到 0 只鸭子!,拉布拉多:找到 3 只鸭子!

猎犬:找到 0 只鸭子!,拉布拉多:找到 4 只鸭子!

猎犬:找到 0 只鸭子!,拉布拉多:找到 0 只鸭子!,牧羊犬:找到 1 只鸭子!

回到您的代码:当Labrador线程(线程 1)唤醒时,它会更新Labrador'sducksfound并发送SIGUSR1. 有人(我们稍后会详细讨论谁)看到了信号和barks所有Dogs. 但唯一Labrador改变的是线程 1 中的那个。RetrieverShepherd线程(分别是线程 2 和 3)还没有看到对Labrador's的更新ducksfound

那么为什么一开始ducksfound打印的值是正确的呢?由于您安装信号处理程序的方式。你在整个进程范围内安装了它——记得我说%SIG的是克隆到你的线程的东西之一。因此,每个线程都有一个处理程序,用于USR1导致所有Dogsto bark。当您发送USR1$$时,此时恰好处于唤醒状态的线程会捕获它。碰巧发送信号的线程是清醒的线程。

这就解释了为什么当Retriever抓到第一只鸭子时,它的ducksfound值是正确的,而Labrador's 不是。 Retriever在线程 2 中捕获鸭子,该鸭子发送SIGUSR1给它自己,然后发送给它barks的所有Dogs. 但在线程 2 中,Labrador从未更新,因此树皮显示 0Labrador和 1 Retriever

使用以下方法可以相当简单地解决非共享变量的问题threads::shared

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

现在,当一个线程更新 a 时Dog,所有线程都会看到它,因此哪个线程正在为信号提供服务并不重要。这很好,因为在您的代码中,“主线程”(线程 0)永远不会重新获得控制权。这可能没问题,但可能会导致比您预期的稍微奇怪的行为。

如果您确实希望存在一个管理器线程,则可能需要显式生成它:

# 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));

请注意,仅在不共享您的情况下放入管理器线程Dogs会导致线程唤醒以打印一堆零。你需要同时做这两件事才能得到你期望的结果。

于 2013-02-14T17:28:23.653 回答