読者です 読者をやめる 読者になる 読者になる

中年engineerの独り言 - crumbjp

LinuxとApacheの憂鬱

Rubyコンテキストスイッチの不思議

仕事上、ROMAに関わっているので、稀にはRubyもやります。

今回検証に使ったソースは此方
https://github.com/crumbjp/Personal/tree/master/study/ruby

こんな感じで動くはずです

 gem install eventmachine
 bash run.sh

Rubyスレッドのコンテキストスイッチの話

Rubyのスレッドはご存じの様に、1.8系は疑似スレッド、1.9系ではネイティブスレッドだけれど並列処理はしない(逐次実行)実装となっている。

疑似スレッド(green threads)
OS上のスレッドを利用せずメインスレッドをRubyインタプリタで切り替えている。
本当の意味での並列処理を行わない。

コンテキストスイッチはsignalがトリガとなっており、特定出来ないタイミングで切り替わる(※と思っていた)

しかし特にExtensionの処理中にはsignal unsafeやmutexがあるかもしれない。
この辺りの挙動をしっかり把握しておきたかった。


検証コード(スレッド駆動)

やたら複雑ですが、、
スレッド1(t1),スレッド1(t2)を並列実行平行実行させてるだけです。(※指摘により修正)
rubyはこの辺りがセンシティブでいけない・・・

関数:test1 , test2 の実装による挙動の違いを調べます。

begin
  t1 = Thread.new {
    Thread.stop
    for i in [0,1,2,3,4]
      test1 i
    end
  }
  Thread.pass
  t2 = Thread.new {
    Thread.stop
    test2
  }
  Thread.pass
  t1.run # Thread1 start 
  sleep 1 
  t2.run # Thread2 start 
  t1.join
  t2.join
rescue
  p $!
end
検証1 (Rubyの世界)

単純にスレッドの挙動を押さえておく

puts '=== Pure ruby mode ==='
def test1(i)
  puts 'test1 ('+i.to_s+') start'
  sleep 1
  puts 'test1 end'
end

def test2
  puts 'TEST2 START'
  sleep 3
  puts 'TEST2 END       **** HERE ****'
end

結果

=== Pure ruby mode ===
test1 (0) start
test1 end
test1 (1) start
TEST2 START
test1 end
test1 (2) start
test1 end
test1 (3) start
TEST2 END       **** HERE ****
test1 end
test1 (4) start
test1 end

ちゃんと(疑似)並列に動いています。

検証2 (Cの世界)

検証1のsleepをExtension(C実装)に持ってくるとどうなるでしょう?

puts '=== Extension mode ==='
require 'myext'
include MyExt
def test1(i)
  myext1 i
end
def test2
  myext2
end
VALUE method_test1(VALUE self,VALUE vi) {
  int i = NUM2INT(vi);
  printf("test1 (%d) start\n",i);
  fflush(stdout);
  sleep(1);
  printf("test1 end\n");
  fflush(stdout);

  return Qnil;
}

VALUE method_test2(VALUE self) {
  printf("TEST2 START\n");
  fflush(stdout);
  sleep(3);
  printf("TEST2 END       **** HERE ****\n");
  fflush(stdout);
  return Qnil;
}

結果

=== Extension mode ===
test1 (0) start
test1 end
test1 (1) start
test1 end
TEST2 START
TEST2 END       **** HERE ****
test1 (2) start
test1 end
test1 (3) start
test1 end
test1 (4) start
test1 end

Extension実行部(TEST2 START => TEST2 END)は完全に排他されています。

考えてみると、これは当然の実装で
rubyから見るとExtensionの先にどの様な処理があるか解りません。特にmutexがあるとデッドロックしてしまう。
よってExtension中でコンテキストスイッチを行う訳にはいかない。

検証3 (Ruby&Cの世界)

一応、Extension同士が干渉している可能性を排除しておきましょう。

test1 の方はPure ruby

puts '=== Complex mode ==='
def test1(i)
  puts 'test1 ('+i.to_s+') start'
  sleep 1
  puts 'test1 end'
end
require 'myext'
include MyExt
def test2
  myext2
end
VALUE method_test1(VALUE self,VALUE vi) {
  int i = NUM2INT(vi);
  printf("test1 (%d) start\n",i);
  fflush(stdout);
  sleep(1);
  printf("test1 end\n");
  fflush(stdout);

  return Qnil;
}

結果

=== Complex mode ===
test1 (0) start
test1 end
test1 (1) start
test1 end
TEST2 START
TEST2 END       **** HERE ****
test1 (2) start
test1 end
test1 (3) start
test1 end
test1 (4) start
test1 end

やっぱり変わりません。

検証4 (EventMachineの世界)

RubyといえばEventMachine!(?)という事でEventMachineの挙動も調べます。
ご存じの様にEventMachine先はselect , epoll の様な待ち受け処理です。

検証2、検証3の結果、待ち受け中はrubyは完全に止まってしまうハズ。
しかし現実は横にスレッド立てて別の処理をしたり出来ます。

何故だ!!?


まずは挙動を確認

puts '=== EventMachine mode ==='
def test1(i)
  puts 'test1 ('+i.to_s+') start'
  sleep 1
  puts 'test1 end'
end

require 'eventmachine'
class Echo < EM::Connection
  def receive_data(data)
    send_data(data)
  end
end
def test2
  puts 'TEST2 START'
  EM.run {
    EM.add_timer(3) {
      EM.stop
    }
    EM.start_server("0.0.0.0", 9999, Echo)
  }
  puts 'TEST2 END       **** HERE ****'
end


結果

=== EventMachine mode ===
test1 (0) start
test1 end
test1 (1) start
TEST2 START
test1 end
test1 (2) start
test1 end
test1 (3) start
test1 end
test1 (4) start
TEST2 END       **** HERE ****
test1 end

何故だーーー!!????

ruby自身がExtension内でのコンテキストスイッチを許していないのに
EventMachineの待機中にはちゃんとスイッチされてる!!

可能性としては

  1. Extension中にも条件を満たせばコンテキストスイッチをする
  2. EventMachineが何かしてる

多分2.なんだろうな・・・

という事でこれからEventMachineを追ってきます。
何か解ったら次回!!

次回:Rubyコンテキストスイッチの不思議・その2 - LinuxとApacheの憂鬱