2018-12-05 (Wed) [長年日記]
_ 特殊変数のスコープ
(この記事はRuby Advent Calendar 2018の参加記事です。)
Rubyはシンプルな文法が特長である。
Rubyには特殊変数と呼ばれる変数があって、見た目はグローバル変数だが、$_
や $&
などの一部の変数はローカル変数に似たスコープを持っている。「似た」というのは厳密には違いがあって、基本的にブロックローカルではなくメソッドローカルなのだが、スレッドのブロックではスレッド毎に固有の値を持つ。
t = Thread.start {
Thread.current.name = "sub"
$_ = "foo"
5.times do
puts "#{Thread.current.name}: #$_" #=> 5回とも「sub: foo」と出力
sleep(0.1)
end
}
Thread.current.name = "main"
$_ = "bar"
5.times do
puts "#{Thread.current.name}: #$_" #=> 5回とも「main: bar」と出力
sleep(0.1)
end
t.join
この挙動は、単純にメソッドローカルにするとスレッドセーフでなくなってしまうからである。 では、以下のように同じProcオブジェクトを別々のスレッドで実行するとどうなるか?
def with_special_var(x, &block)
Thread.current[:x] = x
block.binding.eval("$_ = Thread.current[:x]")
block.call
end
f = -> {
5.times do
puts "#{Thread.current.name}: #$_"
sleep(0.1)
end
}
t = Thread.start {
Thread.current.name = "sub"
with_special_var("foo", &f)
}
Thread.current.name = "main"
with_special_var("bar", &f)
t.join
実は処理系によって挙動が異なる。
CRubyの場合:
$ ruby -v t.rb
ruby 2.6.0dev (2018-11-28 trunk 66060) [x86_64-linux]
main: bar
sub: foo
main: bar
sub: foo
main: bar
sub: foo
main: bar
sub: foo
main: bar
sub: foo
JRubyの場合:
$ jruby -v t.rb
jruby 9.2.0.0 (2.5.0) 2018-05-24 81156a8 OpenJDK 64-Bit Server VM 9-Debian+0-9b181-4bpo91 on 9-Debian+0-9b181-4bpo91 +jit [linux-x86_64]
main: foo
sub: foo
main: foo
sub: foo
main: foo
sub: foo
main: foo
sub: foo
main: foo
sub: foo
JRubyでは両方のスレッドでProcオブジェクトを生成した環境の $_
が共有されるが、CRubyの場合はProcオブジェクトを生成した環境とスレッドを生成した環境が同一のメソッド呼び出し(上記の例ではトップレベル)だった場合、 $_
は各スレッド毎に別々の値を持つ。
CRubyでは特殊変数へのアクセスは以下のように実装されている。
static inline struct vm_svar *
lep_svar(const rb_execution_context_t *ec, const VALUE *lep)
{
VALUE svar;
if (lep && (ec == NULL || ec->root_lep != lep)) {
svar = lep[VM_ENV_DATA_INDEX_ME_CREF];
}
else {
svar = ec->root_svar;
}
VM_ASSERT(svar == Qfalse || vm_svar_valid_p(svar));
return (struct vm_svar *)svar;
}
lep
はlocal environment pointerでメソッドローカル変数が格納されている領域を指す。
ec->root_lep
は各スレッドの実行環境のトップレベルのメソッドローカル変数が格納されている領域を指す。
両者が同じ値の場合、各スレッドのトップレベルの特殊変数を格納する ec->root_svar
を返している。
JRubyの実装は読んでいないが、おそらく上記のように特殊変数のアクセス時に動的なチェックを行うのではなく、スレッド生成時にブロックが直接記述されている場合だけを特別扱いしているのだろう。
以下のようにProcオブジェクトとスレッドを別の環境で生成した場合は、CRubyでもJRubyと同じようにスレッド間で $_
の値が共有される。
def with_special_var(x, &block)
Thread.current[:x] = x
block.binding.eval("$_ = Thread.current[:x]")
block.call
end
def make_proc
-> {
5.times do
puts "#{Thread.current.name}: #$_"
sleep(0.1)
end
}
end
f = make_proc
t = Thread.start {
Thread.current.name = "sub"
with_special_var("foo", &f)
}
Thread.current.name = "main"
with_special_var("bar", &f)
t.join
Rubyはシンプルな文法が特長である。