オープンソースのオブジェクト指向プログラミング言語「Ruby」の文法を一から学ぶための入門連載。最新版の2.1に対応しています。今回は、スレッドを扱うクラスや軽量スレッド、「グルー言語」でもあるRubyからプロセスを操るさまざまなメソッドの使い方などを解説します。
前回の「RubyのFile/IOクラスで入力と出力、ファイルの読み取りと書き込み、フィルター作成」では、データの入出力について解説しました。連載第11回に当たる今回は、スレッド、ファイバー、プロセスといったトピックについて解説します。
ThreadクラスやFiberクラスを使うことで、Rubyでも並列処理を記述することが可能です。また、前回紹介したIOクラスを使うことで、Rubyプログラムからプロセスを生成して入出力を行えます。
さまざまなプログラミング言語では並列処理のための仕組みが使えるようになっています。もちろんRubyも例外ではなく、ThreadクラスやFiberクラスを使って並列プログラムを書くことができます。
また、RubyからOS固有のコマンドを別のプロセスとして実行したり、自分自身をfork(フォーク)して別プロセスとしてRubyプログラムを実行したりすることができます。
RubyではThreadクラスを利用することで、比較的簡単にスレッド処理を記述できます。しかし現在の実装では、並列化による処理速度向上を狙ってThreadクラスを利用することは難しいです。
1.9でOSのネイティブスレッドを利用するようになりましたが、過去のライブラリとの互換性を保つため、基本的に同時に実行されるスレッドは1つに限定されています。ただし、Ruby公式のThreadクラスのページによると、入出力による待ち時間を有効利用することを狙ったスレッド化は有効であるようです。
実際にスレッドを作り、その動作を観察してみましょう。thread01.rbにサンプルコードを示します。
names = %w(alice white-rabbit cheshire-cat) threads = [] names.each do |name| threads << Thread.new do 3.times do |i| print "#{name}:#{i}, " end end end threads.each { |t| t.join } puts "\nall threds are terminated."
$ ruby thread01.rb alice:0, white-rabbit:0, cheshire-cat:0, alice:1, alice:2, white-rabbit:1, white-rabbit:2, cheshire-cat:1, cheshire-cat:2,
このサンプルでは、1行目で「names」変数にスレッドの名前を表す文字列の配列を格納し、2行目でThreadオブジェクトを入れておくための配列「thread」を用意しています。
3行目の「each」メソッドで配列から名前を1つずつ取り出し、4行目の「Thread.new」によって新たなスレッドを作っています。作成したスレッドの中で行いたい処理は、「Thread.new」にブロックとして渡します。ここでは、6〜8行目のように、3回スレッド名とともにインデックスを標準出力に流しています。
生成したスレッドは、直ちに非同期的に実行されるので、スレッドを生成した元のスクリプト(ここではthread01.rb)では、スレッドの終了を待つ必要があります。そのため、12行目のように「Thread#join」メソッドを使って、スレッドの終了を待っています。全てのスレッドの処理が終われば、14行目の「puts」が実行されてスクリプトの実行は終了します。
thread01.rbを何度か実行して、そのたびに出力内容が変わることに注意してください。どのような順序でスレッドが実行されるかは、OSやRubyインタープリターによって決められるからです。
スレッドを使いたくなる典型的なシチュエーションとしては、無限ループを回しつつ、ユーザーの入力を待つというようなパターンです。このような場合、無限ループ処理とユーザーの入力を待ち続けるような処理を、2つのスレッドに分けることで実装できます。ここでは、スレッドを使って簡単な時計を作ってみましょう。
require "io/console" view_thread = Thread.new do loop do print "\033[2K\r#{Time.now.strftime("%F %T")}" sleep 1 end end input_thread = Thread.new do while STDIN.getch != "q"; end puts view_thread.kill end view_thread.join input_thread.join
まず1行目で「io/console」をrequireしていますが、これは11行目で標準入力から1文字入力したときに、エンターキーなしで文字を取り込むために必要となります。
3〜8行目では、時計を表示するためのスレッドを生成して、変数「view_thread」に格納しています。このスレッドは、「loop」による無限ループによって、ひたすら1秒おきに現在の時刻を表示します。
5行目の「print」の引数の「\033[2K」は見慣れないかもしれませんが、これはターミナルに「画面をクリアせよ」という指示を送るための制御コードです。また、続く「\r」は復帰を表す文字なので、画面をクリアしたのちカーソルが左端に移動します。
続く「#{}」内での処理で「Time#strftime」メソッドを使って、今日の日付と現在時刻を表示しています。その後6行目の「sleep」メソッドで1秒待ち、ループの先頭に戻ります。
10〜14行目では、標準入力を待ち受けるためのスレッドを生成して、変数「input_thread」に格納しています。このスレッドの中では「STDIN.getch」でキーボードからの入力を待ち受け、それが文字qでない限り、whileループによって再度待ち受け状態になります。文字qが入力されれば、「puts」メソッドで改行を出力したのち、13行目のように「Thread#kill」メソッドを使って「view_thread」を終了させ、「input_thread」自身も終了します。
16行目と17行目では、thread01.rbの場合と同様、各スレッドの終了を待ちます。「input_thread」はqキーを押すことによって終了し、その際「view_thread」も終了するので、その時点でプログラムは終了します。
Copyright © ITmedia, Inc. All Rights Reserved.