2006.09.10

REALbasicのマルチスレッドと同期機構関連機能

ファイルのダウンロードなど、時間がかかる処理を実行する際、単にメソッドを呼び出しただけでは処理終了まで操作不能になってしまう。
時間がかかる処理はスレッド内で実行すると、そのようなことにはならない。
ぜひともスレッドを使おう。

普通のメソッドをスレッド化する手順

1.プロジェクトウインドウで右クリック、追加からクラスを選択等の方法でプロジェクトにクラスを追加する。

2.「Class1」となっているデフォルトのクラス名から名前を適当に変更し、追加したクラスの「Super」を「Thread」にする。説明の便宜上「myThread」というクラス名にしたと仮定。

3.「myThread」内にイベントハンドラ「Run」が現れるので、ここにボタンの「Action」等に書かれている(処理に時間のかかる)メソッドをコピペする。メソッド内にオブジェクトへの参照が含まれる場合は、適切な形に直す必要がある。

例) EditField1.Text = "hoge" → Window1.EditField1.Text = "hoge"

4.今までメソッドを直接記述していたボタンの「Action」には、例えば以下のように書く。

Dim t As New myThread
t.run

これで、これだけでスレッドとして実行できる。

スレッドに引数を渡す

当然、スレッドに引数を渡したくなるだろう。
しかし、runイベントには引数を渡せない。
myThread内にプロパティを作り、それを引数代わりに使うのだと思われる。
例えば、「s」というString型のプロパティを作ったとする。
値を代入しておき、しかる後にrunするといった感じになる。

Dim t As New myThread
t.s = "test"
t.run

runイベントの中では、プロパティ「s」の値を使えばいい。

myThread内にメソッドを追加できる。スコープ次第で外から引数を渡しつつ実行できるが、この場合はスレッドとしての実行にならないので注意。
runイベントから呼び出す分にはOK。

複数のスレッドを実行する

この辺りから情報が怪しくなってくるので注意。
まー日本語で書かれた情報が無いよりはいいでしょう?

同じスレッドをたくさん実行したい場合、どうするか?
myThread型の配列を宣言したくなるが、これはうまくいかない。
myThread型の変数に対して繰り返しNewすればいいようだ。

dim t As myThread
dim i as integer

for i = 0 to 10
t = new myThread
t.s = "test" + str(i)
t.run
next

ただ、これだと複数スレッドの実行自体はうまくいくものの、特定のスレッドを操作できないので不都合がある。
これ以上のことはよく分からん。

スレッドのメソッドとプロパティ

最近のREALbasicはスレッド関係が強化されていて、プロパティ、メソッドともに増えている。
詳細はリファレンス参照。

プロパティ

Priority: 優先度。引数で与えられた数字が大きいほど高いCPU占有度で実行される。引数は1〜10
StackSize:
State: スレッドの状態を4段階で表現
ThreadID: UNIXにおけるプロセスIDみたいなもの。スレッドそれぞれにユニークな番号が割り振られる

メソッド

Kill: スレッドを停止
Resume: Suspend状態のスレッドを再活性化
Run: 実行
Sleep: 引数に与えられたミリ秒分スレッドを一時停止。タイムアウト後は自動的に再活性化
Suspend: スレッドをResumeが実行されるまで永久的休眠状態へ移行

REALbasicの三つの同期機構について

マルチスレッド、またはアプリケーションの多重起動が想定される環境で共有のリソースにアクセスする際には、スレッド/プロセス間の衝突を防ぐための同期機構が必要になる。
REALbasicには、Semaphore、CriticalSection、Mutexの三つが用意されている。

Semaphore

Window等のプロパティ等、スレッドから見えるところにパブリックスコープでSemaphore型の変数を用意しておき、

sm as Semaphore

Openイベントなどスレッド実行前にインスタンスを作っておく。

sm = new Semaphore

そして、myThread内の、共有リソースへのアクセスを行なうメソッドの前後を、

sm.Signal
sm.Release

で囲む。例えば、

sm.Signal
// ファイルへ書き出し処理
sm.Release

などとする。イメージとしては、Signalが入場券発行所、Releaseが入場券回収所みたいな感じだ。
その「入場券」は、有限のものとして設定される。同時に「場内」に進入できるのは「入場券」の枚数次第となる。

Semaphoreのコンストラクタに渡す引数が、その「枚数」設定。
引数は、管理するリソースの数に合わせて設定する。例えば五つまで同時アクセス可能なデータベースへのアクセスを行なうメソッドを制御するなら、

sm = new Semaphore(5)

などとしてインスタンスを生成する。これによって、実行が同時五つまでに抑制される。デフォルトは1。

あるスレッドがsm.Signalを実行した時点でSemaphoreの値が0の場合は、Semaphoreが1以上になるまで保留状態に置かれ、そのスレッドの処理はそこで止まり、Semaphoreが1以上になった時点で処理が再開される。
TDLの人気アトラクションで、入場制限がかかっている状態を連想するといい。

CriticalSection

コンストラクタに引数を渡さないSemaphoreのデフォルトとほぼ同じ動作。
Semaphoreと違って、コンストラクタに引数は渡せない。管理できるリソースの数は一つだけ。
Signalの代わりにEnter、Releaseの代わりにLeaveを使う。

Windowオブジェクト等にcriticalSection型のPropertyをパブリックスコープで作る。

cs as criticalSection

Semaphore同様Openイベント等でスレッド実行前にインスタンスを生成しておく。

cs = new CriticalSection

そして、myThread内の、共有リソースへのアクセスを行なうメソッドの前後を、

cs.Enter
cs.Leave

で囲む。ほとんどSemaphoreと同じ。

CriticalSectionはSemaphoreと違って、複数回Enterできる。複数回のEnterからリソースを開放するには、同数のLeaveが必要。
再帰処理において使うのだと思われる。

Mutex

最近のREALbasicにはMutexもある。
アプリケーション内のみならず、OSワイドなスコープを持つ。
同じマシン上で実行されているREALbasicアプリケーションから参照可能。
複数のアプリケーションから一つのリソースを使用する際の調停や、多重起動を抑止するために使える。

Appオブジェクト内にプライベートなスコープのMutex型のPropertyを作る(これがミソ)。

mx as Mutex

AppオブジェクトのOpenイベントハンドラでインスタンス生成。

mx = new Mutex("myapp")

CriticalSection同様の使い方をするほか、TryEnterメソッドの返り値を判定して多重起動を抑止するなど。
既に同じ引数で生成されたインスタンスがあれば、それらとの間で同期処理が行なわれる。

if not mx.TryEnter Then
MsgBox "Please only use one instance of My Spiffy Application at a time"
Quit
end if

CriticalSection同様、Enterを複数回実行可能。もちろん、リソースの開放には、同回数のLeaveが必要。

関連リンク

TaskMng

まつもとひろゆきさんのスレッドクラス。とても便利そうだけど、いまいち自分の脳みそがどういう実装にしていいか思いついてくれない。
オブジェクト指向への理解が足りないからだと思われる。

暗号化されていないので、ソースを読み解いた暁にはスレッドへの真の理解が開けるのではないか。

OOP University:パート 26

REALbasic university Japanのスレッドの回。

|

2005.08.26

[REALbasic]REALbasicでのAppleEventと、そのサンプル

ソースを見たいという人が居たので、最近連綿とやっていたSafariのAppleEventによるコントロールをモジュール化したものを公開してみます。というかSafariStalkerの中の人です。

「AppleEventSample.sit」をダウンロード

実際はSafariだけじゃなくて、System EventsなどにもAEを送ってたりしますけど。

初心者なもんで恥ずかしいですが、国内においては情報のソースがのきなみ数年前のもの、書籍は皆無! っていう風前の灯火状況にあるREALbasic界に、少しでもにぎわいが戻ればという思いを込めて。

REALbasicでAppleEventをやる際には、かの田中求之氏によるドキュメントが、もはや6年近く前のものになるものの、今なお最高の参考資料になります。

>REALbasic で AppleEvent を活用する

>REALbasic で AppleEvent を活用する方法や、AppleScript 対応のアプリケーションの作り方を解説した文書です。
>pratical_use_of_AE_in_RB.sit

↓ドキュメントはここにあります。

http://mtlab.ecn.fpu.ac.jp/webcon_resources.html

このドキュメントを読んだ限りでは、


core¥setd{
data:"http://www.google.co.jp/", //←これ
----:obj {
form:'prop',
want:'prop',
seld:'pURL',
from:obj {
form:'indx',
want:'docu',
seld:1,
from:'null'()
}
},
&csig:65536
}

こういうパターンが出たときにどうしていいのか分からなくて焦りましたが、

ae.StringParam("data") = "http://www.google.co.jp/"

こんな風にAppleEventのStringParamとして渡してやればいいです。

|

2005.08.19

[REALbasic]続・SafariをREALbasicからAppleEventで操る

なんでか、以下のAppleEventの返り値はUTF16になっていて、これをhttpSocketクラスのGetメソッドに直接渡すとハングしてしまう。

ReplyStringで返ってきたものをTextConverterでUTF8に直してからGetに渡すとうまくいく。
くそー、足かせ一つ追加。ASCII文字列もエンコーディング関係あるんだっけ?

■Safariから最前面のウインドウで表示中のページのURLを取ってくる

Dim ae As AppleEvent
Dim obj As AppleEventObjectSpecifier
Dim c As TextConverter

obj=GetIndexedObjectDescriptor("docu", Nil , WinNum)
ae = NewAppleEvent("core","getd","sfri")
ae.ObjectSpecifierParam("----") = GetPropertyObjectDescriptor(obj, "pURL")

If ae.send() Then
c = GetTextConverter(Encodings.UTF16, Encodings.UTF8)
Return c.convert(ae.replyString)
End If

------------------------------

よーし、大物きたー。

■Safariのテキストエンコーディングのメニュー選択

Dim ae As AppleEvent
Dim objT, objU, objV, objW, objX, objY, objZ as AppleEventObjectSpecifier
Dim c As TextConverter
c = GetTextConverter(GuessJapaneseEncoding("テスト"), Encodings.MacJapanese)

//SafariをActivate
ae = NewAppleEvent("misc","actv","sfri")
if ae.send() then
end if

//メニューを選ぶ
ae = NewAppleEvent("prcs", "clic", "sevs")
objZ = GetNamedObjectDescriptor("pcap", Nil, "Safari")
objY = GetIndexedObjectDescriptor("mbar", objZ, 1)
objX = GetNamedObjectDescriptor("mbri", objY, c.convert("表示"))
objW = GetNamedObjectDescriptor("menE", objX, c.convert("表示"))
objV = GetNamedObjectDescriptor("menI", objW, c.convert("テキストエンコーディング"))
objU = GetNamedObjectDescriptor("menE", objV, c.convert("テキストエンコーディング"))
objT = GetNamedObjectDescriptor("menI", objU, c.convert("日本語(ISO 2022-JP)"))
ae.ObjectSpecifierParam("----") = objT

If ae.send() then
end if

GetIndexedObjectDescriptorで表現する場合は、ObjectDescriptorの部分が次のようになる。

objZ = GetNamedObjectDescriptor("pcap", Nil, "Safari")
objY = GetIndexedObjectDescriptor("mbar", objZ, 1)
objX = GetIndexedObjectDescriptor("mbri", objY, 5)
objW = GetIndexedObjectDescriptor("menE", objX, 1)
objV = GetIndexedObjectDescriptor("menI", objW, 14)
objU = GetIndexedObjectDescriptor("menE", objV, 1)
objT = GetIndexedObjectDescriptor("menI", objU, 5)

AppleEventに固執するのはよくないけど、あのベンチ結果を目の当たりにすると粘りたくもなる。

|

2005.08.18

[REALbasic]AppleEvent VS. AppleScriptどっちが速い?

AppleEventで実装した処理と、AppleScriptをプロジェクトに埋め込んだ場合とで、どれほどパフォーマンスに差が出るのか、簡単に調べてみた。

比較した処理は、Safariから表示ページのsourceを取ってきて、EditFieldに表示するというもの。

■AppleScript陣営

dim s1 as double
dim s2 as double

s1 = microseconds
EditFIeld1.Text = getsource
s2 = microseconds
editfield3.seltext = "AppleScript:" + chr(9) + str(s2 - s1) + chr(10)

※getsource.scrptの内容

on run {}
tell application "Safari"
tell document 1 to source
end tell
end run

■AppleEvent陣営

Dim ae as AppleEvent
Dim obj as AppleEventObjectSpecifier

Dim s1 as Double
Dim s2 as Double

s1 = microseconds

obj=GetIndexedObjectDescriptor("docu", nil , 1)
ae = NewAppleEvent("core","getd","sfri")
ae.ObjectSpecifierParam("----") = GetPropertyObjectDescriptor(obj, "conT")

If ae.send() then
s = ae.replyString
EditFIeld1.Text = s
end if

s2 = microseconds
editfield3.seltext = "AppleEvent: " + chr(9) + str(s2 - s1) + chr(10)

----------------------------------
結果発表(数字が大きい方が遅い)

Case1. ちょっと重めのページを表示させた状態

 AppleEvent: 210,350
 AppleScript: 309,295

 AppleScriptはAppleEventの1.4倍遅い

Case2. めちゃ軽いページ

 AppleEvent: 10,188
 AppleScript: 44,691

 AppleScriptはAppleEventの4.38倍遅い

(それぞれ30回の平均値)

やはりAppleEventを直接発行した方が速い。
重い処理ほど差が少なくなっている結果が出ている。
データの転送量が問題にならない処理では、もっと差が広がるだろう。

一応、試してみた。ウインドウの数を返す処理においては、AppleScriptはAppleEventの7倍遅かった。
コンディションによっては、10倍差が開くこともあった。

AppleEventで返り値のあるイベントを発行する場合、AppleEventの場合は返り値が型付きなので、例えばIntegerとして利用する場合は返り値が全部StringになってしまうAppleScript経由の取得より、キャストが必要無い分、さらにパフォーマンスの差が広がるものと思われる。

これに加えてAppleEvent直発行の方が安定性が高いのだから、いやー、もうこりゃ使い捨てじゃない処理はみんなAppleEventで実装って感じかね。AppleScriptは好きですが。

|

[REALbasic]特定のアプリケーションプロセスの存在の有無をAppleEventで確認する in Mac OS X

AppleEventを送る際、SystemEventの送信先が居ないとハングするっぽいので、対象プロセスが居るかどうか確認する必要がある。
それっぽいメソッドが見つけられなかったので、やっぱりAppleEventで実装。トホホ。

ScriptDebuggerで以下のスクリプトを実行。

tell app "finder" to exists application process "Safari"

AppleEventのログを見ると、Finder(MACS)からは値を返しておらず、System Eventsに下請けに出しており、そこから値が返ってきていることが分かる。Mac OS Xになってからは、Finderではプロセス管理をやらなくなっているようで、AppleEventは、最初からSystem Eventsに送信する必要がある。FinderにAppleEventを送っても、System Eventsへの引き継ぎはやってくれずnullとか0とかしか返してこない。

ScriptDebuggerのログでは、AppleEventの送信対象を記録してくれないので、ちとハマった。System Eventsのクリエータタイプってどうやって調べんのよ(笑)。

"/System/Library/CoreServices/System Events.app"に実体があることが分かったので、
"System Events.app/Resources/SystemEvents.scriptSuite"に、AppleEventCodeの記載があることを確認。めでたく"sevs"にAppleEventを送ればいいことが分かった。Cocoaでscriptableなアプリはこの手で調べられるみたい。Carbonアプリは、旧来の手法が使えるはず。

Dim ae as AppleEvent
Dim obj As AppleEventObjectSpecifier
ae = NewAppleEvent("core","doex","sevs")
obj = GetNamedObjectDescriptor("pcap", nil, "Safari") //プロセス名で探す
obj = GetPropertyObjectDescriptor(obj, "pvis") //Visible = Trueのみに
ae.ObjectSpecifierParam("----") = obj

If ae.Send() Then

If ae.ReplyBoolean = True Then
editfield1.text = "起動中"
Else
editfield1.text = "起動してない"
End If
End If

ちなみに、リファレンスのGetOrdinalObjectDescriptorに載っているサンプルコードも、Mac OS Xではもはやobsoleteで動かないはず。以下に書き換えればOK。

Dim a as AppleEvent
Dim i, count as Integer
a = NewAppleEvent("core", "cnte", "sevs")
a.MacTypeParam("doex") = "prcs"
a.ObjectSpecifierParam("----") = GetOrdinalObjectDescriptor("prcs", nil, "all ")
If a.Send then
count = Val(a.ReplyString)
End if
editfield1.text = str(count)

|

[REALbasic]Mac OS Xでは、GlobalFloatingWindowのFloaterProcessが有効にならない問題

Safariがfrontmostのときだけウインドウを表示しようと思って、WindowをGlobalFloatingWindowに設定し、WindowクラスのFloaterProcessを指定したところ(下記をOpenなどのイベントに書いておけばいいはず)、これが有効にならない。

Window1.FloaterProcess = "sfri"

何か自分が変なことをしているのかもと調べたところ、以前から解決されていないREALbasicのバグだと知る。

http://support.realsoftware.com/listarchives/realbasic-nug/2004-09/msg00226.html←ここで話題が出ている。

しょうがないので、Timerでpollingすることに。またAppleEventか。

■Safariがfrontmost(最前面のウインドウ)かどうか問い合わせる

Dim ae as AppleEvent
Dim obj As AppleEventObjectSpecifier
ae = NewAppleEvent("core","getd","macs")
ae.ObjectSpecifierParam("----") = GetNamedObjectDescriptor("pcap", Nil, "sfri")

//SafariのfrontmostがTrueだったらWindow1を表示。そうでなければ隠す。
//むろん、ApplicationのOpenか、Window1のOpenにme.hideとかしとく。
If ae.Send() Then
If ae.ReplyBoolean = True Then
Window1.Show
Else
Window1.Hide
End If
End If

|

2005.08.17

[REALbasic]SafariをREALbasicからAppleEventで操る

Safariのやることを常時監視したい。
パフォーマンスや安定性が重要になるので、REALbasicからのコントロールは、ダイレクトにAppleEventで実装することにする。
AppleEventのログを取るのにもScriptDebuggerが活躍してくれた。
やってみた感じ、パスパスとデータを取ってきてくれて結構速い気がする。AppleScriptを埋め込んだ場合とmicrosecondsを取って比較してみようかな。

■システムのバージョンを得る(GUI Scriptingのため)

Dim ae as AppleEvent
ae=NewAppleEvent("fndr", "gstl", "sevs")
ae.StringParam("----") = "sysv"
if ae.send() then
if ae.replyInteger >= 4144 then
editfield1.text = "OS Version Check OK"
end if
end if

■UI ScriptingがEnableかどうか

Dim ae as AppleEvent
ae=NewAppleEvent("core", "getd", "sevs")
ae.ObjectSpecifierParam("----") = GetPropertyObjectDescriptor(nil, "uien")
if ae.send() then
if ae.replyBoolean then
editfield1.seltext = "UI Scripting Enable"
else
editfield1.seltext = "UI Scripting Disable"
end if
end if

■SafariをActivateする(GUI Scriptingのため)

Dim ae as AppleEvent
//SafariをActivate
ae = NewAppleEvent("misc","actv","sfri")
if ae.send() then
end if

■ウインドウの数を取ってくる

Dim ae as AppleEvent

ae = NewAppleEvent("core","cnte","sfri")
ae.macTypeParam("kocl")="docu"

If ae.send() then
EditFIeld1.Text = str(ae.replyInteger)
end if

■前面のドキュメント中のURLを取ってくる(8.19訂正)

Dim ae As AppleEvent
Dim obj As AppleEventObjectSpecifier
Dim c As TextConverter

obj=GetIndexedObjectDescriptor("docu", Nil , WinNum)
ae = NewAppleEvent("core","getd","sfri")
ae.ObjectSpecifierParam("----") = GetPropertyObjectDescriptor(obj, "pURL")

If ae.send() Then
c = GetTextConverter(Encodings.UTF16, Encodings.UTF8)
Return c.convert(ae.replyString)
End If

■前面のドキュメント中のhtmlを取ってくる

Dim ae as AppleEvent
Dim obj as AppleEventObjectSpecifier

obj=GetIndexedObjectDescriptor("docu", nil , 1)

ae = NewAppleEvent("core","getd","sfri")
ae.ObjectSpecifierParam("----") = GetPropertyObjectDescriptor(obj, "conT")

If ae.send() then
EditFIeld1.Text = ae.replyString
end if

■前面のドキュメント中のウインドウ名(title)を取ってくる

Dim ae as AppleEvent
Dim objA as AppleEventObjectSpecifier

objA=GetIndexedObjectDescriptor("docu", nil , 1)

ae = NewAppleEvent("core","getd","sfri")
ae.ObjectSpecifierParam("----") = GetPropertyObjectDescriptor(objA, "pnam")

If ae.send() then
EditFIeld1.Text = ae.replyString
end if

■開いているすべてのウインドウ名(title)を取ってくる

Dim ae as AppleEvent
Dim i As Integer
Dim objA as AppleEventObjectSpecifier

objA = GetOrdinalObjectDescriptor("docu",NIL,"all ")

ae = NewAppleEvent("core","getd","sfri")
ae.ObjectSpecifierParam("----") = GetPropertyObjectDescriptor(objA, "pnam")

If ae.send() then
ListBox1.DeleteAllRows
for i = 1 to ae.replyDescList.count
ListBox1.addrow ae.replydescList.stringItem(i)
next
end if

|

2005.08.12

[REALbasic]REALbasicでシェルコマンドのフロントエンドを作る

REALbasicで、シェルコマンドのフロントエンドを作りたい。なおかつ、システムにデフォルトでは存在しないコマンドなので、そのシェルコマンドもバイナリの中にバンドルしたいと思って以前2ちゃんねるで聞いてみたところ、「できない」というのがその場での結論だった。

しかし、その後方法を見つけた(みんな知っているの?)ので書いておこう。そういうニッチなことを簡単に解説しているサイトも日本には無いようだし。

REALbasicは、Mac OS Xにおいて、ビルドするバイナリの形式をいろいろ選べる。
Mac OS Xのみ互換のMach-O形式を指定すると、例の拡張子が.appのパッケージの形態を取ることになる。

.appの実態はフォルダなので、中に簡単にバンドルするファイルを入れておくことができる。
ここまで分かれば、後はやることが決まったも同然だ。


dim f as folderItem
dim a as new application
dim s as new shell

//実行ファイルの参照を取得
//(MyApplication.app:Contents:MacOS:MyApplication)
f = a.executableFile

//実行ファイルがあるフォルダの参照を取得
//(MyApplication.app:Contents:MacOS)
f = getfolderItem(f.absolutePath).parent

//実行ファイルがあるフォルダのPOSIXパスを取得。
//さらにバンドルしたシェルコマンドまでのパスを末尾に結合し、
//(MyApplication.app/Contents/MacOS/unison)
//実行する。
s.execute(f.shellPath + "/unison")

//コマンドからの標準出力を取得
editField1.text = s.result

こんなコードを書いてビルド。できたアプリケーションをControl + Clickして「パッケージの内容を表示」。
Contents、MacOSとフォルダをたどって、そこに(MacOSフォルダの中に)シェルコマンドを入れておく(例ではunison)。
このShellPathというFolderItemのプロパティはマニュアルに無いんだけど、何なんだろう? AppleScriptの「POSIX path of 」みたいなのを探して、外人が使っているのを見て初めて知った。
このほか、マニュアルに記載の無いFolderItemのプロパティにPathTypeShellというのがあるけど、これは/shallow/deep/destfileという形式で与えられた引数をFolderItemとして扱うためのもののようだ。コンソールアプリを作るときに役に立つ。ちょっと話がそれた。

http://rbgazette.com/?p=238

後はこれの応用だけど、困るのはデバッグ。
ただ単にシェルコマンドをプロジェクトウインドウに放り込んでおくだけでは、Resourcesフォルダにリソースとして保存されてしまう。リソースをシェルコマンドとして実行するメソッドは無いようだ。

こうなると、ビルドの度にMyApplication.debug/Contents/MacOSフォルダの中にシェルコマンドをコピーしてやらないといけない。そのこと自体はシェルスクリプトを組むなり何なりで自動化は可能だけど、ほかに方法があるのではと思う次第。

|