2013年5月30日~6月1日の3日間、お台場にある東京国際交流館にてRubyKaigi 2013が開催されています。基調講演をそれぞれレポートします。
2日目の基調講演の演者はJose Valimです。司会の角谷さんにより「再起動したRubyKaigiの基調講演に最もふさわしい人物の一人」と紹介をされたJoseは、Rubyにおける並行プログラミングの可能性について話しました。
自己紹介
Joseは、2006年からRubyを書き始めたそうです。それからOSSにも深く関わっており、2010年からRails coreチームにジョインしています。そして、Elixirという言語の作者でもあります。ElixirはErlangのVMの上で動作するスクリプト言語で、並行プログラミングと相性が良く、強力なメタプログラミングの機能を持っています。現在のバージョンは0.9.0となっています。 現在はplataformatecに所属しています。
どうして、いま、並行性(concurrency)なのか
Joseはまず、並行性をテーマとした背景を説明しました。
かつて、それぞれのサーバ上のCPUのコアは一つでしたが、いまや数多くのCPUコアを持ったサーバを利用することは非常に現実的な選択となりつつあります。例えば、50コアを持ったサーバが2,600ドルで購入可能な例を説明しました。
そして、かつてはCPUの性能向上に比例してソフトウェアが速くなっていたという事情を説明しました。コア数が1つでCPU自体のクロック数を向上させれば良かった時代には、プログラムの側もそのような「フリーランチ」の恩恵を受けていたとのことでした。ですが、メニーコアが前提となったプログラミングにおいては、プログラム自体をそのようなアーキテクチャに対応させなければいけない、そのような状況は未来のものだと思われていたが、その「未来」はもう来ているのではないか、と話しました。
プログラム自体をそのようなメニーコアに対応させる場合、Rubyの場合はスレッドを利用して実現することになりますが、その中でもいろいろな問題が起こりうることを示しました。
クラスのアクセサにおけるレースコンディション
一つの問題は、スレッドによるレースコンディションです。
単純なクラス変数へのアクセサを経由した代入を考えてみます。例えば、WebアプリケーションのUser.current_user
という変数があるとします。
そして、コントローラーで次のような処理をしているとします。
User.current_user
にユーザをセット …(1)
少し時間のかかる処理を実行する …(2)
ユーザにメールを送る …(3)
しかしながら、このような処理を行うと、例えば(1)?(3)がユーザごとに直列で実行されているような場合には問題は無いのですが、いくつかのスレッドを同時に動かしている状況においては、問題のある挙動をしてしまう場合があります。
具体的には、ユーザー「Jose」がアクセスして、その(2)の処理が動いている間に別のユーザー「Matz」がアクセスしてしまうと、current_user
が置き換わってしまい、結果的にJoseに送るべきメールがMatzに送られてしまうということが起こりうると説明しました。
このようなUser.current_user
は、「 共有されていて、変更可能な状態で、グローバル変数のようなものだ」とのことです。
そして実は、そういう風にセットできる値がRailsにはかなりの数存在し、例えばActionMailer::Base.from
、ActionController::Base.logger
などがそうだとのことです。実はJose自身のライブラリであるDeviseなどにもそのような設定項目はあります。
様々な実装の存在するRuby - 実装による違い
Joseは、Rubyには様々な実装が存在することを紹介しました。そして、スレッドセーフとは、実装によって意味が様々に変わってくるという主張をしました。
例えば、RailsのActionView::Resolver
が内部で行っているように、ある変数にHashオブジェクトを代入し、そのハッシュをキャッシュのように利用することが、Rubyのプログラムではしばしば行われます。
ここで問題があります、では、このHashオブジェクトはスレッドセーフなのでしょうか?
実は、この答えは実装によって変わります。YARVではyes、そしてJRubyやRubinius 2.0ではnoとなります。実はYARVではGVL(Giant VM Lock、OSレベルのスレッドを一つしか走らせないロック)が存在するため、複数スレッドからのハッシュへのアクセスは安全に行われるのです。しかしそのような機構が無いほかの言語では、複数のスレッドからハッシュへのアクセスが発生することが起こり得ます。
そのため、例えばRailsでは、実装の間での差異を吸収するためにthread.rbに定義されているMutexを導入しています。ですが、Joseはそのような方法は個人的にでは古くさい、好きではないと言います。あまりにも低レイヤーの実装を、開発者にさせてしまうことになるからです。
この手の低レイヤーなプログラミングについて、次のように表現しました。
コードのメンテナンス性が下がる
YARVのパフォーマンスも下がる
開発者の幸福度も下がる!
そこで、適切な、並行プログラミングのためのセマンティクスをRubyに定義することが必要であると述べました。
ほかの言語から学ぶこと - Rubyへの提案
そのためにほかの言語から学んでみよう、として、いくつかの言語の実装例を紹介しつつ、全部で4つの提案をしました。
Javaに目を向けてみる
冒頭に、Joseは、Rubyのスレッド処理関連のライブラリと、Javaの並行処理関連のライブラリを比較して見せました。
まず、thread.rbには主に以下の5つのクラスが定義されていることを示しました。
スレッド
Mutex
状態変数
キュー
サイズ付きキュー
ところが、Javaにはもっともっとたくさんの並行処理関連のライブラリがあります。例として java.util.concurrent
ネームスペースの下には、20個を超える様々な用途のためのクラスが細かく定義されている図を見せました。
そして、これはエラーにしてしまいたいぐらい(ETOOMANYCLASSES)の多さであると、冗談めかして指摘しました。
提案その1:Hash#concurrent_read, Hash#concurrent_write
その中でも、一つの例としてConcurrentHash(Map)を取り上げました。
Railsやその関連のライブラリの中にはスレッドセーフでないHashオブジェクトの使い方があふれているし、ことHashをキャッシュに使う場面は非常にポピュラーだとして、ここで再び、YARV以外の実装ではHashオブジェクトはノンスレッドセーフであることを強調しました。
そこで、一つの案として ConcurrentHash
を用いた例を紹介しました。キャッシュのためのオブジェクトにただHashリテラル {}
を代入するのではなく、ConcurrentHash.new
としています。
しかし、このやりかたは、「 1つのクラスが支配する」 、つまり用途ごとにクラスを細かく分けすぎないというRuby的な大クラス主義の原則に反しているとしました。その代わりのものとして、Hash#concurrent_read
や Hash#concurrent_write
を提案しました。
これらのメソッドを用いることで、実装によってデフォルトではHashオブジェクトが並行読み込み/書き出しに対して安全であるかどうかを判定することが可能になります。例えばYARVでは両方がtrue
、JRubyではfalse
となります。
その一方で、あらゆる実装でスレッドセーフな挙動に切り替えが可能なようにするそうです。具体的には、スレッドセーフにさせたい場面で明示的にHash#concurrent_read!
、Hash#concurrent_write!
を呼び出すことにする、というAPIを提案しました。
提案その2:AtomicReferenceの導入
また、AtomicReference
クラスの導入についても取り上げました。
複数の操作を同時に行うような場合、どういうタイミングで行われても結果が正しいことが保証されていることを、アトミック性(不可分性)と言います。
例えば1から4の数字を加算する場合1 + 2 + 3 + 4 = 10
になることをプログラマは意図するでしょう。この場合、1つずつ処理をすれば当然結果は10になります。しかし、並行プログラミングの世界では、加算の処理方法を誤ると、実行途中の値に加算をすることとなってしまい、結果が10以下の数字になってしまうことがあります。そこで、1つずつ操作した場合と同じ結果が必ず得られるように注意してプログラミングしなければなりません。"1つずつ操作した場合と同じ結果が必ず得られる"とき、1つずつの操作にアトミック性があると言います。
AtomicReference
は複数のスレッドから操作されるようなオブジェクトに対して、その操作が並行になった場合でもアトミックにしたい場合に利用するクラスです。
Joseユーザの名前のような単純なインスタンス変数についての例を考えます。単純にプログラムをすると、実装によっては、スレッド内部で同時に変更の操作をすると、途中の状態に対する操作が行われてしまい、結果として意図した値にならない場合があります。
これに関して、例えばYARVでは(GVLの力で)定義も更新もアトミック性が保証されていますが、JRubyでは定義する側は問題は無いものの、更新する場合には問題が起こり、結果が保証されない場合があります。
そこで、AtomicReferenceとしてインスタンスにオブジェクトを関連づけ、AtomicReferenceを経由した操作とすることで、制御を抽象化し、アトミック性を保証することを提案しました。
class User
def initialize
@name = AtomicReference .new
end
def name
@name .get
end
def name=(name)
@name .set(name)
end
end
また、atomic_accessor :name という、アトミックな参照を簡単に定義するためのクラスメソッドを導入することも一緒に提案しました。
Joseは、そもそもインスタンス変数は、デフォルトではunsafeなものとして宣言するすべきだということ、そしてスレッドセーフなRubyコードはそれらをそういうunsafeものとして取り扱うようにすると良いのではないかということを話しました。実際、現在のYARVはそういったコードを問題なく扱えるわけですが、そういうコードが動いてるのは「たまたま」なのであるとのことです。
AtomicReferenceをthread.rbで定義されるクラスに追加して、必要なときは、それを利用してアトミックな変数を扱えるようにすべきだ、という提案でした。
さらに、AtomicReference#compare_and_swap
の導入も一緒に提案しました。これは、現在の値が意図したものかを判定して更新の可否を制御するメソッドで、lock-freeなデータの取り扱いをRubyで実装するために第一に必要なものだとのことでした。ちなみに、Javaにも同様なAtomicReference#compareAndSet
が存在します。
提案その3:Go言語の紹介と軽量で高機能な新しいQueueの採用
ここで、そういった並行性や原始的な操作などの取り扱いに長けている言語として、Go言語を紹介しました。Go言語は、シンプルかつパワフルな抽象化を保ったまま、より進んだことができることを教えてくれる、とのことでした。
そして、RubyもGoのように、「 メモリの上でシェアするのではなく、コミュニケーションでシェアする」モデルであるべきだ、つまりメモリ上のミュータブルな状態にアクセスして制御するのではなく、メッセージングによって制御を行うべきだと言います。
ここで、別スレッドで変数の状態を変えるサンプルをRubyのコードで示しました。
is_done = false
Thread .new {
# 何か時間のかかる処理
is_done = true
# その後の処理
}
sleep(0.5 ) until is_done
puts :DONE
一方で、Goは、チャンネルとコルーチン(goroutine)を第一級市民としているそうです。JoseはつづいてGo言語でのチャンネルを使ったサンプルを紹介しました。チャンネルを利用した場合、プログラムはスレッドセーフとなります。
実はRubyには、SizedQueueクラスを用いることで、同じようなコードを書けなくはないと言い、そちらを利用したRubyのサンプルも提示しました。
Queueの機構が存在するのだから、もっとそれを使ってGoで言うチャンネルのような操作を上手にやれるはずだ、とのことです。そこで3つ目の提案として、より軽量で高機能なQueueの実装をすることを主張しました。
提案その4:さらに軽量な、並行プログラミングのための仕組みを組み込みで提供する
各言語における軽量スレッドの実装を最後の提案で紹介しました。ここで3つ目のほかの言語として、Erlangを紹介しました。
Erlangには軽量プロセスが、GoにはGoroutineがそれぞれ組み込みのものとして存在しています。一方で、Rubyにおけるスレッド利用のコストはその程度になるでしょうか? 一般的にはこれらの言語の軽量スレッドと比べ、コストがかかるものとなるでしょう。
なので、4つ目の提案としては、もっとパワフル、軽量で、並行性があり、スケジュール化されているプリミティブなクラスを持つべきだと言及しました。 実は、これには既に具体的な実装として、Celluloid が存在しています。 Joseは、Actor modelは「設計図に関することだ」として、Ruby自身が発展するためには、Actor modelを用いた「設計図」を簡単に利用できるようにするべきだ、と提案を締めくくりました。
結論 - concurrentなRubyのための議論をしよう
Joseはここまでなるべく具体的な例を出しながら、4つの並行性に関する提案を行ったあと、発表の結論として次のようにまとめました。
まず、いくつか存在している処理系の間で、セマンティクスや挙動が違うという現状が、Rubyの並行プログラミングをよけいに難しくしているのではないか、としました。
それと同時に、今まで、我々プログラマ自身も並行の考え方に不慣れであったのではないか、と問いかけました。我々は、「 並行に考える」ことについての学ぶ機会をよりいっそう設けるべきだとのことです。
そうしたことから、Rubyがconcurrentな言語になるためには、次のようなものが必要だと言及しました。
十分に定義されたセマンティクス
スレッド安全な標準ライブラリ
高度な抽象化と、その教育(例えば、不可分操作やキュー、Actorについて)
そして最後に、Jose自身が目標としていることは、最終的な答えを提示することではないのだ、と述べました。Joseは、このキーノートを通して議論を喚起し、Rubyのconcurrencyの取り扱いについて、より多くの考えが出てくることを望んでいるとのことでした。
Joseの発表内容と提案は非常に高度な内容でしたが、その中でもRubyの可能性のために学べること、考えるべきことへの示唆を非常に多く含んだ、キーノートにふさわしい内容であったと思います。