Squeak Smalltalk 向けの分散処理関連で遊ぶ


どう書く?org - 分散関数呼び出し に触発されて、これまで苦手にしてきた…というかほとんど知識のない通信・分散処理系のしくみをいじったのでメモ。例によっていろいろと勘違い and/or そんなことふつーやらねーよ的なことを書いているかもしれませんが、なにとぞご容赦あれ、かし。あと、スクリプトを試す場合は xx.xx.xx.xx を環境に合わせて適当に書き直してください。


■ ソケットを使う

ネット上の情報を得るのにソケットくらいは使ったことがあったので、これに関連したすでに備え付けの機能でなんとかできないものかと Nebraska 周辺を探ってみていると、StringSocket という文字列の配列をやりとりする機能を持つクラスを見つけたので、まずこれで試してみます。

StringSocketTestCase の #setUp → #testBasic の順に見てゆくと必要な手順が分かります。なるほど、ソケットを双方で作って接続し、それぞれのソケットを a StringSocket でラップすれば、あとはストリームのように #next 、#nextPut: で読み書きができる…ということのよう。

通信できるデータは配列に入れた文字列のみ。文字列以外をやりとりしたい場合向けに、StringSocket と同じ抽象クラス ObjectSocket を共有する ArbitraryObjectSocket が用意されています(ただ、プログレスバーを通信のたびに表示する動作はいかにもトロそうなので今回はパス)。実際のデータのやりとりは #connectIO をコールするタイミングで行なわれるもよう。文字列であっても、UTF-8 な日本語の文字列ではうまく送れないっぽいので、 #convertToSuperSwikiServerString/#convertFromSuperSwikiServerString を使用して回避しました。


サーバー側スクリプト
| socket end in pricestring |
pricestring := [:arr |
   | price |
   price := (arr first * (100 - arr second) / 100) asInteger asStringWithCommas.
   arr := arr collect: [:each | each asInteger asStringWithCommas].
   ('販売価格 {1}円 (定価{2}円から{3}%引き)'
      format: {price}, arr) convertToSuperSwikiServerString].
socket := Socket newTCP.
socket listenOn: 9999.
socket waitForConnectionFor: 60.
end := StringSocket on: socket.
[   [(in := end processIO; nextOrNil) isNil] whileTrue.
   in first caseOf: {
      ['pricestring'] -> [end nextPut: {pricestring value: in allButFirst}].
      ['stop'] -> [^end destroy]
   }] repeat
クライアント側スクリプト
| socket end in out timeToRun n |
n := 10000.
timeToRun := [
   socket := Socket newTCP.
   socket connectTo: (NetNameResolver addressFromString: 'xx.xx.xx.xx') port: 9999.
   socket waitForConnectionFor: 60.
   end := StringSocket on: socket.
   n timesRepeat: [end nextPut: #('pricestring' '2000' '20'); processIO].
   out := Array new writeStream.
   n timesRepeat: [
      [(in := end processIO; nextOrNil) isNil] whileTrue.
      out nextPut: in first convertFromSuperSwikiServerString].
   end nextPut: #('stop')] timeToRun.
[end processIO; isConnected] whileTrue.
end destroy.
^{out contents first. out size. timeToRun}
=> #('販売価格 1,600円 (定価2,000円から20%引き)' 10000 79808)

10000 回で 80 秒(1.0Ghz PowerPC, 1.5Ghz VIA C7M; WiFi 11Mbps)。


XML-RPC サーバー/クライアントを使う

サーバーには XML-RPC Server が、クライアントには Spy-XML-RPC があって、これらを組み合わせればそれっぽいことができるっぽい(←よく分かっていない(^_^;))ことが判明。サーバーである XML-RPC ですが、クライアントである Spy-XML-RPC に依存しているので、まず Spy-XML-RPC の動作確認をしてから XML-RPC のセットアップに取りかかったほうがよいみたいです。

サーバー側定義
Object subclass: #XMLRPCDoukaku

XMLRPCDoukaku >> priceString: args
    | price array |
    price := (args first * (100 - args second) / 100) asInteger.
    array := {price}, args collect: [:int | int asStringWithCommas].
    ^('販売価格 {1}円 (定価{2}円から{3}%引き)' format: array)
        convertToSuperSwikiServerString
サーバー側スクリプト
| ma xmlrpc |
xmlrpc := XMLRPCHttpModule new.
ma := ModuleAssembly core.
ma alias: '/xmlrpc' to: [ma addPlug: [:req | xmlrpc process: req]].
(HttpService startOn: 9999 named: 'httpd') plug: ma rootModule.
XMLRPCServerRequest
    registerService: 'doukaku.pricestring'
    class: XMLRPCDoukaku
    selector: #priceString:
クライアント側スクリプト
| proxy result n timeToRun |
n := 100.
proxy := XMLRPCProxy new url: 'http://xx.xx.xx.xx:9999/xmlrpc' asUrl.
timeToRun := [n timesRepeat: [
    result := proxy invokeMethod: 'doukaku.pricestring' withArgs: #(2000 20)]
] timeToRun.
^{result convertFromSuperSwikiServerString. timeToRun}
=> #('販売価格 1,600円 (定価2,000円から20%引き)' 35223)

お題で指定された 10000 回はちょっと無理っぽいので 1000 回で 35 秒。


■ rST - Remote Smalltalk を使う

まあ、上の二つは分散処理そのものというよりは、分散処理にも利用できるクライアント/サーバー系を手軽に構築するには…みたいな話だったので、なんかもっとそれっぽいものはないのかと探してみた結果見つかったのがこれ。

この手のものにしては珍しく(そんなんじゃホントは困るのですが…(^_^;))最新の 3.9 への対応もうたっているのでさっそくインストールして試してみることに。手順はざっとこんな感じ。

  1. デスクトップメニュー → save as... → 適当な名前をつけて環境(仮想イメージ)を複製
  2. デスクトップメニュー → open... → Monticello Browser
  3. 上のボタン欄から +Repository → HTTP
  4. FillInTheBlank(入力欄)に SqueakSource の Repositry の呪文(MCHttpRepository location: 'http://www.squeaksource.com/rST' user: '' password: '')をコピペして Accept(s)
  5. http://www.squeaksource.com/rST が追加されたことを確認して上のボタン欄から Open
  6. 開いたウインドウ右枠から DynamicBindings-gk.1.mcz をクリックして選択して Load
  7. 左枠 KomServies → 右枠 KomServices-mp.4.mcz → Load
  8. 左枠 rST → 右枠 rST-Noury.53.mcz → Load
  9. RSTBroker class >> #startOnPort:logging: の self stop をダブルクオート $" で括ってコメントアウト
  10. ワークスペースなど適当な場所で World findATranscript: nil. RSTSamples serverStartup; runClient を do it (入力、選択後、alt/cmd + d)
    • トランスクリプトに everything is ok! と表示されれば動いている…っぽい(^_^;)
    • RSTBroker reset を do it して既存のブローカーを停止
  11. デスクトップメニュー → save and quit
    • ネット上の別のマシンに仮想イメージ(.image)と対のチェンジセット(.changes)をコピーし、双方で起動
      • サーバー側の手順は RSTSamples class >> #serverStartup を、クライアント側は同 #runClient が参考になりそう


ということで、サーバー側に適当なクラス(Doukaku)を作って、そのクラスメソッドに呼び出したいメソッド #pricestring:discount: を定義してみる。

Object subclass: #Doukaku

Doukaku class >> pricestring: priceInt discount: discInt
    | result data |
    result := (priceInt * (100 - discInt) / 100) asInteger.
    data := {result. priceInt. discInt} collect: [:int | int asStringWithCommas].
    ^'販売価格 {1}円 (定価{2}円から{3}%引き)' format: data

サーバー側のブローカーを起動して、Doukaku を doukaku としてエクスポート。

RSTBroker startOnPort: 9999.
RSTBroker export: Doukaku named: 'doukaku'

クライアント側ブローカーも同様に起動。

RSTBroker startOnPort: 9999

クライアント側からの呼び出し。

| doukaku timeToRun result |
doukaku := 'doukaku@xx.xx.xx.xx:9999' asLocalObject.
timeToRun := [1000 timesRepeat: [
    result := doukaku pricestring: 20000 discount: 20]
] timeToRun.
^{result. timeToRun}
=>  #('販売価格 16,000円 (定価20,000円から20%引き)' 19677)

やはり 10000 回は無理っぽくて 1000 回で 20 秒。



どれを投稿しようか考え中…。w (追記: 結局、スクリプトの簡潔さと分散処理っぽさをとって rST 版にしました)



追記

■ OperaORB-native を使う

コメント欄で umejava さんに OperaORB のご紹介をいただいたので、さっそく試してみました。rST より高速ですし、何よりインストールが SqueakMap 経由でできて簡単なのが素人には嬉しいです。

  1. デスクトップメニュー → save as... でイメージを適当な名前で保存
  2. デスクトップメニュー → open... → SqueakMap Package Loader → OperaORB-native
  3. 3.9 にはない SocketStream>>#resetOutStream、#resetInStream を #resetBuffers へ書き換え
    • RmtConnector>>#sendData:timeout: の resetOutStream を resetBuffers に書き換えて accept (alt/cmd + s)
    • RmtConnector>>#receiveOneMessage の resetInStream を resetBuffers に書き換えて accept
  4. デスクトップメニュー → save and quit でイメージを保存して終了
  5. 仮想イメージ(.image)と対のチェンジファイル(.changes)をクライアント側にコピー
  6. 双方のマシンで当該イメージを起動


rST のときと同様に、クラス Doukaku とそのクラスメソッドにメソッド #pricestring:discount: を定義し、#doukaku としてエクスポートしました。

サーバー側定義
Object subclass: #Doukaku

Doukaku class >> pricestring: priceInt discount: discInt
    | result data |
    result := (priceInt * (100 - discInt) / 100) asInteger.
    data := {result. priceInt. discInt} collect: [:int | int asStringWithCommas].
    ^'販売価格 {1}円 (定価{2}円から{3}%引き)' format: data
サーバー側スクリプト
OperaORB init: #((port: 9999)).
OperaORB root initialize.
OperaORB root at: #doukaku put: Doukaku
クライアント側スクリプト
| doukaku result timeToRun |
doukaku := OperaRemoteObject name: #doukaku host: 'xx.xx.xx.xx' port: 9999.
timeToRun := [10000 timesRepeat: [
    result := doukaku pricestring: 2000 discount: 20]
] timeToRun.
^{result. timeToRun}
=> #('販売価格 1,600円 (定価2,000円から20%引き)' 86500)

10000 回で 86.5 秒。rST よりはるかに高速です。dRuby 版の #2223 を同じ環境で動かすと 68 秒だったので、負けてはいますが桁違いに遅い…というふうでもなさそうなのでほっとしました。umejava さん、ありがとうございます!