将ithreads与Memoize一起使用时出错

时间:2012-01-28 13:27:31

标签: multithreading perl

我刚刚将线程引入了Perl程序,其中一个模块正在使用Memoize。 我收到此错误消息:

  

线程1异常终止:在禁用标量上下文中调用匿名函数;断裂

如果我同时拥有线程和Memoize,则会发生错误,但如果我带走其中一个元素,则会消失。但问题不在于Memoize不是线程安全的 - 在我的代码中,所有的memoization都发生在同一个线程中。

这是Memoize的错误吗?有没有办法可以解决这个问题?否则我将摆脱Memoize。

以下是一些隔离问题的示例代码:

use strict;
use warnings;
use threads;
use Thread::Semaphore;
use Memoize;

my $semaphore = Thread::Semaphore->new;

memoize('foo');
sub foo {
    return shift;
}

sub invoke_foo {
    $semaphore->down; # ensure memoization is thread-safe
    my $result = foo(@_);
    $semaphore->up;

    return $result;
}

my @threads;
foreach (1 .. 5) {
    my $t = threads->create( sub { invoke_foo($_) });
    push @threads, $t;
}
$_->join foreach @threads;

3 个答案:

答案 0 :(得分:4)

Memoize将每个memoized函数的缓存存储在一个哈希中(而不是使用闭包)。它使用函数的地址作为该哈希的索引。

问题是当函数克隆到新线程时,函数的地址会发生变化。 (在print(\&foo, "\n");中添加invoke_foo。)。这是Memoize中的一个错误。

解决方法:从线程中加载已记忆的模块。以下模拟(相关方面):

use strict;
use warnings;
use threads;
use Memoize;

sub foo {
    return shift;
}

sub invoke_foo {
    return foo(@_);
}

my @threads;
foreach (1 .. 5) {
    my $t = threads->create( sub {
        memoize('foo');
        invoke_foo($_);
    });
    push @threads, $t;
}
$_->join foreach @threads;

顺便说一句,每个线程都有自己的缓存。这也可以被视为一个错误。

答案 1 :(得分:2)

如上所述,Memoize不是线程感知的。如果你想要每线程记忆,ikegami的重组将很好。如果您想要全局记忆,那么用以下内容替换Memoize可能有效:

use strict;
use warnings;
use 5.010;
use threads;
use threads::shared;

sub memoize_shared {
    my $name = shift;
    my $glob = do {
        no strict 'refs';
        \*{(caller)."::$name"}
    };
    my $code = \&$glob;
    my $sep  = $;;
    my (%scalar, %list) :shared;

    no warnings 'redefine';
    *$glob = sub {
        my $arg = join $sep => @_;
        if (wantarray) {
            @{$list{$arg} ||= sub {\@_}->(&$code)}
        }
        else {
            exists $scalar{$arg}
                 ? $scalar{$arg}
                 :($scalar{$arg} = &$code)
        }
    }
}

并使用它:

sub foo {
    my $x = shift;
    say "foo called with '$x'";
    "foo($x)"
}

memoize_shared 'foo';

for my $t (1 .. 4) {
    threads->create(sub {
        my $x = foo 'bar';
        say "thread $t got $x"
    })->join
}

打印:

foo called with 'bar'
thread 1 got foo(bar)
thread 2 got foo(bar)
thread 3 got foo(bar)
thread 4 got foo(bar)

上面的memoize_shared函数相当复杂,因为它处理推导列表和标量上下文以及替换命名子例程。有时只需将memoziation构建到目标子例程中就更容易了:

{my %cache :shared;
sub foo {
    my $x = shift;
    if (exists $cache{$x}) {$cache{$x}}
    else {
        say "foo called with '$x'";
        $cache{$x} = "foo($x)"
    }
}}

将memoization构建到子例程确实会使它更复杂,但它会比使用像memoize这样的包装函数更快。它可以让您精确控制如何记忆子程序,包括使用threads::shared缓存等内容。

答案 2 :(得分:1)

Memoize应该在线程下运行,虽然有点慢:

  

“goto& f在线程Perl下运行的方式存在一些问题,可能是因为@_的词法范围。这是一个错误   Perl,直到它被解析,memoized函数会稍微看到   不同的调用者()并且会在线程上执行一点点   perls比无螺纹的perls。“