coq pull-request #9457: Correct W-Ind in Cic description of the reference manual.
ruby の autoload はマルチスレッドで危険といわれるが、ちゃんと動くという話もある。
実際に試してみよう。
x.rb:
autoload :Y, "./y.rb" error = false t1 = Thread.new { Y.new(0) rescue (puts "error: #{$!}"; error = true) } t2 = Thread.new { Y.new(0) rescue (puts "error: #{$!}"; error = true) } t1.join t2.join puts "no error" unless error
y.rb:
# (1) class Y # (2) def initialize(arg) end end
x.rb の中では Y という定数参照のときに y.rb を autoload するようにしていて、 ふたつのスレッドの中で Y.new(0) とメソッドを呼んでいる。
ここで、y.rb の中ではクラス Y を定義している。
どちらかのスレッドの Y の参照により、そのスレッドで y.rb のロードが起こるわけだが、 そのロード中に他のスレッドにコンテキストスイッチしてそっちで Y.new(0) が呼び出されると 何が起きるか。
y.rb を読み込み終わった後でコンテキストスイッチして Y.new(0) が呼び出されるなら何の問題もないが、 中途半端な状態でコンテキストスイッチすると困るかもしれない。
具体的に、危険なポイントはふたつある。
まず、(1) と書いてある class Y という行より前では、Y という定数が定義されていないので、Y.new(0) は uninitialized constant Y というエラーになってしまうだろう。
また、(2) と書いてある def initialize(arg) という行の直前では、Y#initialize が定義されていないので、 Y.new(0) は Y#initialize ではなく BasicObject#initialize を呼び出し、そっちは引数を受け付けないので、 wrong number of arguments(1 for 0) というエラーになってしまうだろう。
どちらも困るが、後者を考えると、Y が定義がちゃんと完成するまで、つまり y.rb を読み終わるまでは、 他のスレッドが Y の参照をしようとしたら待たせておかなければならない。 ここで、y.rb の中では (Y が未完成でも) Y を参照できるので、 y.rb を実行している (ロードしている) スレッドでは Y を参照でき、 それ以外のスレッドでは待たせるという仕掛けが必要になる。
まず、前者を試す。
y.rb:
sleep 0.1 class Y def initialize(arg) end end
というようにして、ruby 1.9 以降で動かしてみる。
% ALL_RUBY_SINCE=ruby-1.9 all-ruby x.rb ruby-1.9.0-0 error: uninitialized constant Y ... ruby-1.9.1-preview2 error: uninitialized constant Y ruby-1.9.1-rc1 no error ... ruby-2.6.1 no error
ということで、ruby 1.9.1 より前では実際に、他のスレッドでは定数が定義されていない状態で動いてしまう問題があったようである。
次に、後者を試す。
y.rb:
class Y sleep 0.1 def initialize(arg) end end
というようにして、ruby 1.9 以降で動かしてみる。
% ALL_RUBY_SINCE=ruby-1.9 all-ruby x.rb ruby-1.9.0-0 error: uninitialized constant Y ... ruby-1.9.0-3 error: uninitialized constant Y ruby-1.9.0-4 error: wrong number of arguments(1 for 0) ruby-1.9.0-5 error: uninitialized constant Y ruby-1.9.1-preview1 error: uninitialized constant Y ruby-1.9.1-preview2 error: wrong number of arguments(1 for 0) ruby-1.9.1-rc1 no error ruby-1.9.1-rc2 error: wrong number of arguments(1 for 0) ruby-1.9.1-p0 error: wrong number of arguments(1 for 0) ruby-1.9.1-p129 no error ruby-1.9.1-p243 no error ruby-1.9.1-p376 error: wrong number of arguments(1 for 0) ... ruby-1.9.1-p430 error: wrong number of arguments(1 for 0) ruby-1.9.1-p431 no error ... ruby-1.9.2-p330 no error ruby-1.9.3-preview1 error: wrong number of arguments(1 for 0) ... ruby-1.9.3-p545 error: wrong number of arguments(1 for 0) ruby-1.9.3-p547 no error ruby-1.9.3-p550 error: wrong number of arguments(1 for 0) ruby-1.9.3-p551 error: wrong number of arguments(1 for 0) ruby-2.0.0-preview1 no error ... ruby-2.6.1 no error
ということで、ruby 2.0.0 より前では実際に定義が不完全な状態で動いてしまう問題があったようである。
何回か試したが、ruby 2.0.0 以降でこれらの問題が発生することは確認できなかった。
もちろん、他の問題がある可能性はある。
たとえば、Y の参照がスレッドによって挙動が異なるということから、 y.rb の中でスレッドを使っていたらどうなるか、 というのは誰でも疑問になるところだと思う。
y.rb:
class Y sleep 0.1 def initialize(arg) end end p Thread.new { Y }.value
というように y.rb の最後で新しいスレッドを作ってその中で Y を参照するようにすると、 以下のように、ruby-2.4.2 より前と以降で挙動が違う。 (deadlock のメッセージはかなり長いので、ruby-2.6.1 以外のは消してある) ruby-2.4.2 以降では deadlock になるが、それより前ではそうはならない。
% ALL_RUBY_SINCE=ruby-2.3 all-ruby --disable-did_you_mean x.rb ruby-2.3.0 error: uninitialized constant Y Y x.rb:5: warning: already initialized constant Y /tmp/a/y.rb:1: warning: previous definition of Y was here ruby-2.3.1 error: uninitialized constant Y Y x.rb:6: warning: already initialized constant Y /tmp/a/y.rb:1: warning: previous definition of Y was here ruby-2.3.2 error: uninitialized constant Y Y x.rb:5: warning: already initialized constant Y /tmp/a/y.rb:1: warning: previous definition of Y was here ... ruby-2.3.4 error: uninitialized constant Y Y x.rb:5: warning: already initialized constant Y /tmp/a/y.rb:1: warning: previous definition of Y was here ruby-2.3.5 x.rb:7:in `join': No live threads left. Deadlock? (fatal) from x.rb:7:in `<main>' #<Process::Status: pid 12753 exit 1> ... ruby-2.3.8 x.rb:7:in `join': No live threads left. Deadlock? (fatal) from x.rb:7:in `<main>' #<Process::Status: pid 12769 exit 1> ruby-2.4.0-preview1 error: uninitialized constant Y Y x.rb:5: warning: already initialized constant Y /tmp/a/y.rb:1: warning: previous definition of Y was here ... ruby-2.4.0-preview3 error: uninitialized constant Y Y x.rb:5: warning: already initialized constant Y /tmp/a/y.rb:1: warning: previous definition of Y was here ruby-2.4.0-rc1 error: uninitialized constant Y Y x.rb:6: warning: already initialized constant Y /tmp/a/y.rb:1: warning: previous definition of Y was here ruby-2.4.0 error: uninitialized constant Y Y x.rb:5: warning: already initialized constant Y /tmp/a/y.rb:1: warning: previous definition of Y was here ruby-2.4.1 error: uninitialized constant Y Y x.rb:5: warning: already initialized constant Y /tmp/a/y.rb:1: warning: previous definition of Y was here ruby-2.4.2 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.4.3 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.4.4 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.4.5 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.5.0-preview1 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.5.0-rc1 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.5.0 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.5.1 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.5.2 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.5.3 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.6.0-preview1 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.6.0-preview2 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.6.0-preview3 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.6.0-rc1 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.6.0-rc2 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.6.0 x.rb:7:in `join': No live threads left. Deadlock? (fatal) ruby-2.6.1 x.rb:7:in `join': No live threads left. Deadlock? (fatal) 4 threads, 4 sleeps current:0x00007fbdc00054a0 main thread:0x0000563fcabce4b0 * #<Thread:0x0000563fcabfb2c8 sleep_forever> rb_thread_t:0x0000563fcabce4b0 native:0x00007fbdd0196b40 int:0 x.rb:7:in `join' x.rb:7:in `<main>' * #<Thread:0x0000563fcabe2e30@x.rb:5 sleep_forever> rb_thread_t:0x0000563fcada2a30 native:0x00007fbdcc3da700 int:0 depended by: tb_thread_id:0x0000563fcabce4b0 /tmp/a/y.rb:6:in `value' /tmp/a/y.rb:6:in `<top (required)>' /home/ruby/build-all-ruby/2.6.1/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require' /home/ruby/build-all-ruby/2.6.1/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require' x.rb:5:in `block in <main>' * #<Thread:0x0000563fcabe2548@x.rb:6 sleep_forever> rb_thread_t:0x0000563fcada85d0 native:0x00007fbdcc1d8700 int:0 x.rb:6:in `block in <main>' * #<Thread:0x0000563fcabe0108@/tmp/a/y.rb:6 sleep_forever> rb_thread_t:0x00007fbdc00054a0 native:0x00007fbdc7efe700 int:0 depended by: tb_thread_id:0x0000563fcada2a30 /tmp/a/y.rb:6:in `block in <top (required)>' from x.rb:7:in `<main>' #<Process::Status: pid 12890 exit 1>
ということで、ruby 2.0 より前では、基本的な仕掛けがそもそも怪しかったことがわかった。 それ以降でも、ruby-2.4.2 でなんらかの修正が加えられていて、成熟していなかったことがわかる。
ruby-2.0.0-p0 は 2013-02-24 で、ruby-2.4.2 は 2017-09-14 なので、それほど昔ではない。 なので、今でも充分に成熟しているとはいえないかもしれない。
ただ、基本的な仕掛けは作り込まれているので、 ロード中にスレッドを使わないものであれば、 そんなに変なことにはならないのではないだろうか、と感じた。
なお、autoload 相当の仕掛けを const_missing で作れるかというと、 おそらく、Y という定数が定義された後でも y.rb の読み込みが終わるまでは他のスレッドを待たせる、 という仕掛けが実装できないのではないか、と思う。 (Y という定数がなければ const_missing が呼び出されて待つことができるが、定義されてしまうと、 参照をとらえることができない。)
そんなに変なことにはならないのではないか、というのは甘かったようで、 なひさんに指摘されて これらはシングルスレッドならば動く (エラーはでない) が、 マルチスレッドだと deadlock になることがある、という例をつくれた。
% cat a.rb class A def a1() end p [:a, A.instance_methods(false), B.instance_methods(false)] def a2() end end % cat b.rb class B def b1() end p [:b, A.instance_methods(false), B.instance_methods(false)] def b2() end end % cat base_a.rb autoload :A, "./a.rb" autoload :B, "./b.rb" A % cat base_b.rb autoload :A, "./a.rb" autoload :B, "./b.rb" B % cat base_ab.rb autoload :A, "./a.rb" autoload :B, "./b.rb" t1 = Thread.new { A } t2 = Thread.new { B } t1.join t2.join % ruby base_a.rb [:b, [:a1], [:b1]] [:a, [:a1], [:b1, :b2]] % ruby base_b.rb [:a, [:a1], [:b1]] [:b, [:a1, :a2], [:b1]] % ruby base_ab.rb [:b, [:a1], [:b1]] [:a, [:a1], [:b2, :b1]] % ruby base_ab.rb Traceback (most recent call last): 1: from base_ab.rb:5:in `<main>' base_ab.rb:5:in `join': No live threads left. Deadlock? (fatal) 3 threads, 3 sleeps current:0x0000558abe708400 main thread:0x0000558abe3564b0 * #<Thread:0x0000558abe383288 sleep_forever> rb_thread_t:0x0000558abe3564b0 native:0x00007f3b0a5a1b40 int:0 base_ab.rb:5:in `join' base_ab.rb:5:in `<main>' * #<Thread:0x0000558abe556b78@base_ab.rb:3 sleep_forever> rb_thread_t:0x0000558abe708400 native:0x00007f3b067e6700 int:0 depended by: tb_thread_id:0x0000558abe3564b0 /tmp/e/a.rb:3:in `<class:A>' /tmp/e/a.rb:1:in `<top (required)>' /home/akr/ruby/o0/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require' /home/akr/ruby/o0/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require' base_ab.rb:3:in `block in <main>' * #<Thread:0x0000558abe556880@base_ab.rb:4 sleep_forever> rb_thread_t:0x0000558abe6d0530 native:0x00007f3b065e4700 int:0 /tmp/e/b.rb:3:in `<class:B>' /tmp/e/b.rb:1:in `<top (required)>' /home/akr/ruby/o0/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require' /home/akr/ruby/o0/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require' base_ab.rb:4:in `block in <main>'
autoload の依存関係にサイクルがあって、異なるところからロードを始めると、 ロックの順番がひっくりかえってデッドロックになるようだ。
ただ、もともと不完全な定義 (a1 しか定義されていない A とか b1 しか定義されていない B) が出てくるので、 本当にこれはいいのだろうかという気はする。
とはいえ、 ロックをもっと粗粒度にして、autoload の処理全体をひとつのロックで排他制御して、 逐次的に処理すれば動く (デッドロックにはならない) ような気はする。
でも、require と autoload が混じっているときでもデッドロックになることがある。
% cat a.rb class A def a1() end p [:a, A.instance_methods(false), B.instance_methods(false)] def a2() end end % cat b.rb class B def b1() end p [:b, A.instance_methods(false), B.instance_methods(false)] def b2() end end % cat base_r.rb autoload :A, "./a.rb" autoload :B, "./b.rb" t1 = Thread.new { require './a' } t2 = Thread.new { require './b' } t1.join t2.join % ruby base_r.rb [:b, [], [:b1]] [:a, [:a1], [:b1]] % ruby base_r.rb Traceback (most recent call last): 1: from base_r.rb:5:in `<main>' base_r.rb:5:in `join': No live threads left. Deadlock? (fatal) 3 threads, 3 sleeps current:0x0000558e10d38530 main thread:0x0000558e109be4b0 * #<Thread:0x0000558e109eb288 sleep_forever> rb_thread_t:0x0000558e109be4b0 native:0x00007f7fcd3f9b40 int:0 base_r.rb:5:in `join' base_r.rb:5:in `<main>' * #<Thread:0x0000558e10bbe880@base_r.rb:3 sleep_forever> rb_thread_t:0x0000558e10d70400 native:0x00007f7fc963e700 int:0 mutex:0x0000558e10d38530 cond:1 depended by: tb_thread_id:0x0000558e109be4b0 /home/akr/ruby/o0/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require' /home/akr/ruby/o0/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require' /tmp/e/a.rb:3:in `<class:A>' /tmp/e/a.rb:1:in `<top (required)>' /home/akr/ruby/o0/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require' /home/akr/ruby/o0/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require' base_r.rb:3:in `block in <main>' * #<Thread:0x0000558e10bbe5d8@base_r.rb:4 sleep_forever> rb_thread_t:0x0000558e10d38530 native:0x00007f7fc143c700 int:0 /tmp/e/b.rb:1:in `<top (required)>' /home/akr/ruby/o0/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require' /home/akr/ruby/o0/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require' base_r.rb:4:in `block in <main>'
ここまで考えると、require も autoload と同じロックで排他制御する必要があるかな。
Ruby Bug #15598: Deadlock on mutual reference of autoloaded constants
Ruby Bug #15599: Mixing autoload and require causes deadlock and incomplete definition.
Debian Bug #922875: xdu: miscounts the size of root directory
xdu の問題はたいした話ではないのだが、 代替物があるかどうか探すと、xdiskusage というのがほぼ同じ表示で、 かつ、この問題がないようだ。
Debian Bug #922943: xdiskusage: segmentation fault by du result which is just 2 lines
[latest]