我刚刚将线程引入了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;
答案 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。“