プリミティブスレッドを使わない並列処理に向けて

| コメント(0) | トラックバック(0) このエントリーを含むはてなブックマーク

福井の技術者協会のメンバーと、スケールアウト、マルチコア、scala、クラウドなどと最新動向について語る事がよくある。

今回は、その内容についてまとめてみた。

 

プリミティブスレッドを使わない並列処理に向けて書いていくが、長文なので、先にまとめておこうと思う。 

「メッセージループを回せるスレッドだけでプログラムできる?」という問いと、もし、Yesと返事が得られると、排他を気にしなくてよくなるかもしれない。そういう自由が得られるパラダイムについて記載する。また、これらは今後重要になるのではないかと考え、記事にした。

ここではプリミティブなスレッドと、設計パターン。また「制約の中の自由」について書いた後、キューを用いたスレッド間通信の例について記載し、上記のことについて記載することとする。 

 

生スレッドを用いた設計

いきなりであるが、生スレッドという言葉を出した。普通に利用しているスレッドを今回はこのような名前で呼んでおく。別に、「焼きスレッド」とか「煮スレッド」とかを出したいわけではない。何を意図しているかというと、特定の概念等を全く前提にしていない、OS、言語、フレームワークが提供するプリミティブなネイティブスレッドの事である。「全く特定の概念を入れていない」という点であり、pthread_createやcreatethreadで生成するスレッドが直感的にイメージ出来た方は、それを思い浮かべてください。

 

生スレッドを使う事に対して

生スレッドを使うと各スレッド間でリソースの共有が発生する。オブジェクト指向であれば、各オブジェクトが配置されているであろうメモリ領域もリソースにあたる。

 

 

 

20091204_object_thread.png

プリエンプティブなマルチスレッドで同じリソースにアクセスすると排他制御が必要になる。例えば、オブジェクトのメンバ関数またはメソッド呼び出しも、それはそのメンバ変数にアクセスする可能性があるわけで、同一オブジェクトの別のメンバ関数であっても、別スレッドから同時に使用する場合には、排他が必要となる。

20091204_object_thread2.png 

この事を考えると、マルチスレッドで利用可能な一般的なクラスを作ることは、大変である事が分かる。また、オブジェクト指向では隠蔽化により内部実装が透けて見えない。逆に言うとマルチスレッドに対して言及がないと利用できないこととなる。

生スレッドを使う経験則

このように生スレッドを用いた設計/実装(プログラミング)に経験則がある。一番分かりやすい例がデザインパターンである。例えば以下の書籍「Java言語で学ぶデザインパターン入門(マルチスレッド編)」にはマルチスレッドのデザインパターンが記載されており経験則が列挙されている。

  • Single Threaded Execution - この橋を渡れるのは、たった一人
  • Immutable - 壊したくとも、壊せない
  • Guarded Suspension - 用意できるまで、待っててね
  • Balking - 必要なかったら、やめちゃおう
  • Producer-Consumer - わたしが作り、あなたが使う
  • Read-Write Lock - みんなで読むのはいいけれど、読んでる間は書いちゃだめ
  • Thread-Per-Message - この仕事、やっといてね
  • Worker Thread - 仕事が来るまで待ち、仕事が来たら働く
  • Future - 引換券を、お先にどうぞ
  • Two-Phase Termination - 後片付けしてから、おやすみなさい
  • Thread-Specific Storage - スレッドごとのコインロッカー
  • Active Object - 非同期メッセージを受け取る、能動的なオブジェクト

詳しくは書籍を参照されたい。

制約があってこその自由

制約と自由、言葉の定義からすると矛盾するように感じるがこの制約こそが我々に自由を与えてくれる。自由といっても制約に則った自由であるが。

たとえ話

私の住んでいる福井県には、 東尋坊 という断崖絶壁がある。国定公園に指定されているので、危険がないように柵が立ててある。この柵は我々の安全を守るために大丈夫な範囲を示している。

もし、この断崖絶壁に柵もなく「自由」にご利用くださいだとすると、どうなるだろうか?崩れる場所があるかもしれないことを考え、公園に来た人は慎重な行動をすることとなり、観光などがしづらくなる。逆に柵という「制約」があると、その柵の中(「制約」の中)では自由に動き回っていい。

確かに柵があると、一部の利用者からすると使いにくいと感じるかもしれないが、その柵を受け入れれる人からすると、凄く助かる。動き回るのに自由度が増したと考えられる。

フレームワークと自由

フレームワークというものは、「こうしなさい」「あーしなさい」と言っているが(制約)、考えられたフレームワークで、フレームワークの思想と今回の目的の間にギャップがなければフレームワーク(制約)は自由を与えてくれる。フレームワークが危険な部分をカプセル化しているからである。

もし、その危険な部分をカプセル化しないフレームワークがあると、たんなる足手まといにしかならない。

生スレッドと東尋坊

先ほどオブジェクト指向においてマルチスレッド化を検討するのは大変だ!と叫んだ。

オブジェクトのメンバ変数は、いわゆるオブジェクト内のスコープであり、関数という視点で見れば、状態を持つグローバル変数と同様の物に見える。(もちろんグローバル変数ほどではないが)

これを踏まえると、生スレッドを用いたマルチスレッドでオブジェクト指向をするというのは、まったくもって「柵のない東尋坊」と例えることが出来るかと思う。

考えられる制約

では自由度を求め制約をかけたい。考えられる制約は、先ほど示したデザインパターンと捉える事ができる。しかし、昨今マルチコア、メニーコア、スケールアウト(スケールアップではない)、クラウド、NUMA、VM という言葉、及び関数型言語などという言葉を聞くと、どうも上記のマルチスレッドパターンをそのまま適応することは難しく考えられる。

新しい制約が出てもおかしくなく、また各技術において上記の制約(フレームワークなど)を提示し始めていると考えられる

新しい考え方

新しい制約と言っているが、実はそれほど新しいと感じないかもしれない。というのは、先のデザインパターン「Producer-Consumer」や「Read-Write Lock」「Future」「Active Object」等を包含した考え方で新しい制約を作っているように感じられる。

あえて新しいという言葉で気を引いてもらうようにしているだけで、昔から考えられた事だと思う。

マルチスレッドとオブジェクト指向

やはり気になることは、オブジェクトの排他制御である。これは、オブジェクトが複数のスレッドから共有されるために、対策しなければならない。

キュー

あるスレッドと別スレッド間でオブジェクトをやり取りする案として、オブジェクトを共有しないという考え方がある。

例えば、あるスレッドから別スレッド間でオブジェクトをやり取りする場合、オブジェクトに所有スレッドという概念を設けてしまい、所有スレッド以外からアクセスできないようにしてしまう。という考え方である。

これを徹底するためにオブジェクトを別のスレッドに渡す時には、マルチスレッド動作が保障される信頼されるキューAPI経由で別スレッドに渡してしまい、オブジェクトの所有スレッドを明確にしてしまう方法がある。

20091204_queue.png 

「Producer-Consumer」パターンだろ其れは!と指摘したくなるかもしれない。

キューという概念

キューという言葉を使ったが、このキューは要求を待たせることが出来る。「Producer-Consumer」パターンでもしキューがいっぱいになると、または空っぽになると、生産者または消費者スレッドがブロックされる。もし、キューのサイズが1つだと、生産者、消費者が同一処理時間出ない限り複数CPUの恩恵をあずかれなくなる。

ただ、キューのサイズが2以上であると、この差を吸収することが出来る。(もちろん、処理時間比に大きな差があると同様に恩恵にあずかれない)

これを例えるなら、歯車の噛み合わせ、ギア比のように考える事ができるかもしれない。片方は早く、もう片方がゆっくりのようなイメージで。

20091204_queue_g.png 

ちなみに、Azure の WebRole,WorkerRole のイメージ図に歯車が使われていたのは、そういう意味で興味深い。

キューに詰めるオブジェクト

キューに詰めるオブジェクトは要求を含むが、その要求の中にスレッド間で共有するオブジェクトが含まれていると、やはり排他の問題がでてくる。時々パフォーマンスの観点で共有という話をする。が、これは本当か!?と叫びたくなる。いや、共有することは問題ないが、その共有の仕方をどうするか!等というのも気になる。

ここでは単純に各オブジェクトをシリアライズしてそれを転送しデシリアライズする方法のみを考えておく。

キューからメッセージを受け取るスレッド

キューからメッセージを受け取るスレッドについて考えてみる。例えば疑似コードだと以下のようになるかと思う。

void TheReciveThreadEntryPoint()
   {
      while( IsDone() )
         {
         message = queue.dequeue();  //キューから要求を受け取る
         message.execute();          //受け取ったメッセージの実行
         }
   }

このwhile( IsDone() ) をメッセージループとでも呼ぼうか。このメッセージループで重要なポイントがある。 message.execute() の処理時間である。この処理時間、もし10秒かかるとすると、次のメッセージは10秒後に受信することとなる。「えっ!」と思うかもしれないが、1つのループはシングルスレッドで動作するので、仕方がない。

逆にこの制約は、ある有益な効果を提供してくれる。このスレッドでのみ利用されているリソースは共有されない。排他しなくていいということである。なぜかというと、メッセージは、必ず同時に実行されないからである。

ここで「message.execute() をワーカースレッドで...」と思うかもしれない。いわゆる「Worker Thread」パターンである。しかし、この方式には問題を内包している。各メッセージ間で共有リソースがない前提が必要である。もしこの前提が崩れると、排他を考えないといけない。また、排他を考えなくてよかったという前提もなくなる。

要求をするスレッド

では、要求をするスレッドはどうなるか?疑似コードで見てみると

void TheRequestThreadEntryPoint()
   {

   queue.enqueue( message );

   }

上記のようになる。もちろん、queue などというオブジェクトは別スレッドへの委託用であり、本来だと見えないほうがよい。この見えなくする技術はいろいろと考えられる。

ここで注目すべきは、queue に メッセージを積んだ後である。大抵積むことが成功したら、enqueue は素早く抜けると考えられる。もちろん、メッセージ処理が終わるまで待つという考え方もあるが、そのようにすると、実際に処理するまでブロックされることになるだろう。

このように考えると、メッセージを投げた後、

  • 素早く帰ってくる
  • 処理が終わるまでブロック 

などという事が考えられる。

しかし、これだと都合が悪く「素早く戻ってきてほしいが、処理が終わったら通知が欲しい」という要求が出てくる。これに対する1つのアイデアが「Future」パターンなどであるが、これもやはり必要になった際にブロックする事が出てくる。

もうひとつが、「コールバック」や「イベントハンドラ」のような概念が出てくる。処理が終わったらイベントハンドラをコールバックしてほしいというのが出てくる。しかし、このコールバック時のコンテキストが気になる。イベントハンドラが呼び出し元の要求元と違うスレッドで呼び出されると、これまたオブジェクトが共有されることになる。

 

20091204_callback.png

例えば、.NETのイベントハンドラには、別スレッドで呼び出される物があり、その場合には Invoke や delegate を使って呼び出し元のスレッドに処理を委譲する事がある。この事を考えると、要求側にもメッセージループが回っているとうれしい事になる。

あるいは、呼び出し元のスレッドで定期的に要求が完了しているかチェックし、完了していたらそのイベントハンドラをコールバックするという事を組み込む必要がある。

この2つの制約

これまでの中で「メッセージループ」を回せるスレッドばっかりだと、排他という概念が減少する!ということである。この制約はきついが、メッセージループを回せると排他しなくていいという「自由」が手に入る。

さいごに

沢山書いてきたが、「メッセージループを回せるスレッドだけでプログラムできる?」という問いである。もし、Yesと回答得られると、排他を気にしなくてよくなるかもしれない。そういう自由が得られるかもしれない。

もちろん、メッセージループ及びキューはライブラリ、フレームワークやライブラリとして実装されている前提で話している。

なぜ、このような事を書いたか?

  • Azure では Role という、メッセージループのようなものが考えられている
  • Azure では キューがある
  • Azure では VM で1つの要求をうけつけるという概念で、VM間のデータに関しては疎結合である
  • Google App Engine では Task という概念が考えられている
  • Google App Engine では、各タスクに 30秒制約がある
  • Google App Engine では、生スレッドが作れない
  • Scala という言語には actor というスレッドのようなモデルと、メッセージループがある。
  • Scala には Future のような考え方や、コールバックのような概念がある(らしい)
    これらは、また別の機会に書きたい。

また、IronRubyで druby を動かそうとしていたかというと、上記のようなメッセージループを実現したかったからである。

参考書籍

ちなみに、以下の本が参考になったので上げておく。

トラックバック(0)

トラックバックURL: http://www.m-tea.info/mt-tb.cgi/33

コメントする

あわせて読みたいブログパーツ

このブログ記事について

このページは、k1ha410が2009年12月 4日 07:00に書いたブログ記事です。

ひとつ前のブログ記事は「はてなの茶碗 桂文珍さん」です。

次のブログ記事は「マルチスレッド対応のキュー」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。