以前に掲載したAppleScriptで並列処理を行うプログラムの改良版です。
1ファイルあたり数千〜数十万行のテキストファイルを数百個処理する必要に迫られ、さまざまな高速化手法を投入してみたものの、通常のAppleScriptでは与えられたファイルを順次実行するだけ。昨今の複数CPUコアが常識化したMacでは、1つのCPUコアが忙しいだけで、他のコアはほとんど遊んでいる状態です。
メニーコアの、無駄に遊んでいるCPUのパワーを絞り出すため、ぴよまるソフトウェアではAppleScriptの並列実行に取り組んできました。

以前に掲載したもの(v1)は、並列処理を行ううえで必要な技術のテストを行うことが目的だったので、実用性はほとんどありませんでした。
今回のv2では、並列処理実行数を柔軟に指定でき、実際に実戦でテストを行って実用性を確認できています。純粋に4スレッド指定した状態で、1スレッド状態の4分の1の時間で処理を終了しています。1時間以上かかる処理が15分程度で終了する、という感じです。

v1では、メイン側とサブ側の間でファイルI/Oを用いてイベントのやりとりを行っていましたが、その後に助言をいただいたりテストを行う中で、「openハンドラ経由でデータをやりとりする場合に、ファイルでやりとりする必要はない」ことが分かってきました(これは知らなかった!)。
コマンド内容を一度ファイルに書き出さず、相手側のアプレットのopenハンドラにlistやrecordを直接渡せるため、信頼性や処理速度の向上が期待されます。
また、v1では(ファイルI/Oの信頼性計測のために)イベントを頻繁にやり取りしていましたが、実際にはそんなに頻繁にイベントのやりとりを行うわけではありません。「一度パラメータを与えれば終了時までイベントのやりとりは行わない」ケースが多いことが予想されます。v2では最初と最後だけイベントをやり取りするようにしてテストを実施しました。

まずは、こちらのAppleScriptを実行形式(実行後終了しない)で保存してください。AppleScriptを呼び出すためのアプレットです。
| スクリプト名:parallelMain_v2_exe |
–ワンショットタイプのAppleScriptによる並列実行テスト v2
property threadMax : 4 –threadの最大数(環境ごとに静的に変えるか、何らかの動的な判断を行うか?) property threadNameList : {} property threadFileList : {} property doneList : {} property asAppName : “thread” –thread側のアプリケーション名 (thread_1〜thread_N) 変更可
property aResList : {} – property resFile : “”
on run –メイン側プログラムの名前を取得(コールバックのために必須) set mePath to path to me tell application “Finder” set myName to displayed name of mePath end tell –読み込むAppleScriptを選択 set aFile to choose file with prompt “並列実行するAppleScriptを選択してください” set aSubObj to load script aFile –指定フォルダ内のファイルを、Thread数で分割する set aFolder to choose folder with prompt “処理対象フォルダを選択してください” set splitList to splitFilePathToThreadNum(aFolder, threadMax) of me –結果を書き出すファイルを指定 set resFile to choose file name with prompt “処理結果を書き出すファイルを指定してください” –thread(アプレット)の書き出し先 set dFol to (path to desktop from user domain) as string set threadNameList to {} –Sub Threadsを書き出す repeat with i from 1 to threadMax set threadName to asAppName & “_” & i as string set asPath to dFol & threadName set asRes to makeASexecutable(aSubObj, asPath) of me if asRes is not equal to true then –何らかの理由でApplet書き出しに失敗した場合 display dialog asRes return else set aThreadFilePath to (asPath & “.app”) tell application “Finder” open file aThreadFilePath –Sub Threadを起動 end tell set the end of threadFileList to (aThreadFilePath as alias) end if –Sub Threadに処理依頼を行う set aRec to {mainName:myName, paramList:contents of item i of splitList} –Threadに渡すパラメータを組み立てる。myNameを渡しているのは、メイン側へのトークバックのため ignoring application responses –ものすごく大事 tell application threadName open aRec –これでThread側は処理開始 end tell end ignoring set the end of threadNameList to threadName (* –処理のピークをずらすために、乱数秒だけウェイトを入れる set randNum to (random number from 0 to 5) delay randNum *) end repeat end run
–thread側からメッセージを受信する on open aData set threadName to subName of aData set aRes to resultList of aData set the end of doneList to threadName set aResList to aResList & aRes –本来ならthreadの終了確認を個数を数えるという野蛮な方式で行うべきではない if length of doneList = length of threadNameList then –threadから受け取ったデータのファイル書き出しなどを行う write_to_file_AsList(aResList, resFile) of me – –thread実行ファイルの削除 tell application “Finder” delete threadFileList end tell tell me to quit end if end open
–ファイルの追記ルーチン「write_to_file_AsList」 (リストとして書き込み) –データ、対象ファイル on write_to_file_AsList(this_data, target_file) try set the target_file to the target_file as text set the open_target_file to open for access file target_file with write permission set eof of the open_target_file to 0 write this_data to the open_target_file starting at eof as list close access the open_target_file return true on error error_message try close access file target_file end try return error_message end try end write_to_file_AsList
–AppleScriptのソースを取得して返す on getASSource(macPath) set aName to (info for macPath size 0)’s name set tmpPath to ((path to temporary items from system domain) as text) & (do shell script “/usr/bin/uuidgen”) & “.txt” set scptText to do shell script “/usr/bin/osadecompile “ & quoted form of POSIX path of macPath return scptText end getASSource
–指定のテキストをAppleScriptとしてコンパイルし、実行可能なアプレットとして出力する on makeASexecutable(aText, outPathAlias) –出力先のaliasからファイル名のみ撮り出す set aStr to outPathAlias as string set fnRes to getLastLayerOfDirData(aStr) of me –指定のファル名に拡張子が適切に付いていない場合には補う if fnRes does not end with “.app” then set fnRes to fnRes & “.app” end if –テンポラリフォルダ内のテキストファイルに書き出す set aFileName to (do shell script “uuidgen”) & “.applescript” set tmpAppName to (do shell script “uuidgen”) & “.app” set aTempFol to path to temporary items folder from system domain set aTempFolPosix to (POSIX path of aTempFol) & aFileName set aTempPathText to (aTempFol as string) & aFileName –write_to_file(aText, aTempPathText, false) of me store script aText in file aTempPathText replacing yes –ファイルキャッシュをHDDにシンクロさせる –do shell script “sync”–> 実行しなくても大丈夫だった –osacompileコマンドでアプレットを生成 –set sText to “cd ” & (quoted form of POSIX path of aTempFol) & ” && osacompile -s -x -o ” & fnRes & ” ” & aFileName set sText to “cd “ & (quoted form of POSIX path of aTempFol) & ” && osacompile -s -o “ & fnRes & ” “ & aFileName try –コンパイル時にアプリケーションの起動を待ったりする可能性があるので、待ち時間を1時間に設定 with timeout of 3600 seconds do shell script sText end timeout on error erMes return erMes end try –テンポラリに書き込んだTextのAppleScriptを消す try do shell script “rm -f “ & quoted form of aTempFolPosix end try –生成したアプレットを指定のフォルダに移動する set origAppPath to POSIX path of ((aTempFol as string) & fnRes) set destPath to POSIX path of getOnlyFolder(aStr) of me & fnRes try do shell script “mv -f “ & quoted form of origAppPath & ” “ & quoted form of destPath on error erMes return erMes end try return true end makeASexecutable
–テキストで与えられたパスデータから、フォルダのみを返す on getOnlyFolder(a) set curDelim to AppleScript’s text item delimiters set AppleScript’s text item delimiters to {“:”} set aList to text items of a set bList to items 1 thru -2 of aList set aText to bList as string set AppleScript’s text item delimiters to curDelim set aText to aText & “:” return aText end getOnlyFolder
–テキストで与えられたパスデータから、最終階層のテキストのみを返す on getLastLayerOfDirData(aData) set aColon to “:” as Unicode text copy aData to bData if bData ends with “:” then set bData to text 1 thru -2 of bData end if set aLen to length of bData set rData to reverse of (characters of bData) set rData to rData as string set dPos to offset of aColon in rData set resData to text (aLen - dPos + 2) thru -1 of bData return resData end getLastLayerOfDirData
–ファイルの追記ルーチン「write_to_file」 –追記データ、追記対象ファイル、boolean(trueで追記) on write_to_file(this_data, target_file, append_data) try set the target_file to the target_file as text set the open_target_file to open for access file target_file with write permission if append_data is false then set eof of the open_target_file to 0 write this_data to the open_target_file starting at eof close access the open_target_file return true on error error_message try close access file target_file end try return error_message end try end write_to_file
–文字置換ルーチン on repChar(origText, targStr, repStr) set {txdl, AppleScript’s text item delimiters} to {AppleScript’s text item delimiters, targStr} set temp to text items of origText set AppleScript’s text item delimiters to repStr set res to temp as text set AppleScript’s text item delimiters to txdl return res end repChar
–指定フォルダ中のファイルのパスをthread数に分ける on splitFilePathToThreadNum(aFolder, threadMax) set fileList to {} repeat threadMax times set the end of fileList to {} end repeat set aFolderStr to aFolder as string tell application “Finder” tell folder aFolder set nameList to name of every file –whose name ends with “.dcm.txt” end tell end tell set subCount to 1 set aLen to length of nameList repeat with i in nameList set j to contents of i set the end of item subCount of fileList to (aFolderStr & j) set subCount to incVal(subCount, threadMax) of me end repeat return fileList end splitFilePathToThreadNum
–値のインクリメント on incVal(aVar, aMax) if aVar < aMax then set aVar to aVar + 1 else set aVar to 1 end if return aVar end incVal
|
|
▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に
|
次に、並列実行されるAppleScriptです。こちらは、普通のAppleScriptとして保存し、アプレット実行時に「並列実行するAppleScriptを選択してください」とダイアログが出るので、その際に指定してください。
この試作品v2では、多数のファイルの処理を行うケースを前提にしており、処理対象ファイルをスレッド数で分割してそれぞれのスレッドに渡すようになっています。
4コアのCore i7の環境で、通常タイプのAppleScriptにくらべて処理時間は大幅に(CPUパワーの許す範囲でスレッド数を増やして)短縮できました。
並列処理で大量のデータを処理する際には、メモリー使用量にも気をつけ、アクティビティ・モニタで監視しながら実行するのがよいでしょう。

▲AppleScript実行中に大量のデータを抱えすぎてえらい状態になっている例
今回は、何度もテスト実行を行いながらスレッド数の最適化を行いましたが、CPUのコア数や世代(Core Duo、Core 2 Duo、Core iなどなど)を取得してスレッド数の自動判定や増減をコントロールできるとよさそうだと思われました。
また、並列処理を行う対象のAppleScriptは、若干書き換える必要がありますが……書き換えを最低限で済ませられるような仕組みも作るとよさそうです(choose fileとかchoose folderのイベントハンドラをAppleScript自身で横取りするとできそう)。
| スクリプト名:testScript |
property mePath : “” property myName : “”
on run set mePath to path to me tell application “Finder” set myName to displayed name of mePath end tell end run
–メイン側からのパラメータ受信用 on open aParamRec set mainProgram to mainName of aParamRec set pList to paramList of aParamRec set aRes to mainProgram(pList) of mainScript of me ignoring application responses –ここが超重要 tell application mainProgram open {subName:myName, resultList:aRes} end tell end ignoring tell application “Finder” set label index of mePath to 6 –なんとなく、視覚的に処理が終わったことを表現してみた end tell tell me to quit end open
script mainScript on mainProgram(pList) –ここに並列処理するプログラムを記述する –ここからダミー処理(無意味) set randNum to (random number from 5 to 10) delay randNum –ダミー処理ここまで return pList end mainProgram end script
|
|
▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に
|