2012-01-28 96 views
5

我刚刚介绍了一个Perl程序的线程,其中一个模块使用Memoize。 我收到此错误消息:使用ithreads与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; 
+2

您运行的是哪个版本的perl? (因为[这个bug]的询问(https://rt.perl.org/rt3/Public/Bug/Display.html?id=79996)。) – Mat 2012-01-28 15:08:48

+0

我使用Perl的草莓与5.12.3 1.02 memoize的。我无法重现该错误。 – stevenl 2012-01-28 15:35:38

回答

4

记忆将每个记忆函数的缓存存储在一个散列中(而不是使用闭包)。它使用函数的地址作为该散列的索引。

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

解决方法:从线程内加载memoised模块。以下可模拟(相关方面)认为:

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; 

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

+0

我刚看到这个[错误报告](https://rt.cpan.org/Public/Bug/Display.html?id=21707)从5年前(仍未解决) – stevenl 2012-01-29 03:48:23

1

memoize的应在线程工作,虽然有点慢:

“有一些问题的方式转到&˚F工作下这可能是因为@_的词法范围,这是Perl中的一个bug,在解析之前,memoized函数将会看到一个稍微不同的调用者(),并且在线程上执行的速度会稍微慢一点点 perls比无螺纹perls。“

2

如上所述,Memoize不是线程感知的。如果你想每个线程memoization,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)" 
    } 
}} 

构建记忆化到子程序的确使它更有点复杂,但它会比使用包装之类的函数memoize更快。它可以精确控制如何记忆子程序,包括使用threads::shared缓存等。