« UCDViewer 1.2.0リリース | トップページ | Delphiで非同期プログラミング・・・おまけ »

2012年4月 3日 (火)

Delphiで非同期プログラミング

Delphiがウンコ環境たる理由は標準で非同期操作がサポートされていないことです(他にも色々ないないだらけで、ウンザリなんですが・・)。JavaではFuture、.NETではIAsyncResultを使ったAPM (Asynchronous Programming Model)、EAP (Event-based asynchronous pattern)や最新の.NET 4ではTaskを使ったTask Parallel Library など非同期処理がお手軽にできるのですが、Delphiには標準でねぇ・・・・

ということで、勉強をかねて色々調べて簡単な実装をしてみました。最初は.NETのIAsyncResultをぱくろうとしましたが、GC(ガベージコレクタ)のない環境では実際に使ってみるとちょい無理あるということがわかったのでしょぼくれていたのですが、いいのを見つけました。Windows8でサポートされるというWinRTのIAsyncInfo(IAsyncAction,IAsyncOperation<TResult>)です(要するに.NETの簡易版Taskクラスじゃ?)。

実装したスレッドプールはワーカースレッド数が固定で変更はできません(最小・最大スレッド数を指定して、負荷に応じて実際のスレッド数をその範囲で増減なんて自分には実装できないので・・)。各メソッドの意味は実際にWinRTを試したわけじゃないですが、だいたい同じだと思うので、そのまんまMSDNのWinRTのWindows.Foundation namespaceWindows.System.Threading namespace の該当する説明を参考に・・

使い方は、TThreadPool.RunAsyncメソッドで非同期で実行する操作をパラメータとして渡します。RunAsyncメソッドを呼んだだけでは非同期操作が開始されません。開始するには戻り値のIAsyncAction.Startメソッドを呼びます。

非同期操作の終了を待ち合わせるにはIAsyncAction.GetResultsメソッドを呼びます。Startメソッドで開始された非同期操作は終了時に必ずIAsyncAction.Completedイベントが発生します(Cancelメソッドでキャンセルされた場合でも発生)。

実用的なアプリは通常、任意の数の非同期操作を行うと思いますが、上記だけじゃ役不足で、そのための仕組みがThreading.AsyncUtils.pasに定義されている、TAsyncGroupクラスです。

非同期操作をグループとして管理するクラスです。TThreadクラスを直接派生させる場合も同じですが、基本、GCのない環境では非同期操作の終了待ち合わせをしないと死にます。例えば、非同期操作の内部で、TThread.Synchronizeメソッドなどで、フォームのメンバにアクセスする場合、非同期操作が完了する前にフォームが閉じられたりすると、アクセスバイオレーションが発生します。TAsyncGroupクラスはグループとして管理するクラスというわりには、何もしない優れものクラスgawkでAddメソッド、Removeメソッドで自前で追加・削除してください・・dashdashShutdownメソッドでグループをシャットダウンします。シャットダウンした後は追加できません。WaitForTerminationで空になるまで待機します。要するにこれで終了待ち合わせを行います。

IAsyncInfo.Startメソッドで開始する前に必ずTAsyncGroup.Addで追加、IAsyncInfo.Completedイベント内などで必ずTAsyncGroup.Removeで削除をペアで・・

グルーピングする粒度は作るアプリの種類や作り方によりけりですが、とりあえず、フォームごとにAsyncGroupを用意すれば十分だと・・例えば、こんな感じで。

デモアプリは現状ありませんが、とりあえず、作りかけで放置している、Tumblrビューワに組み込んだところ、えらい速くなったような気がぁ・・

Async1

組み込む前は1画像毎にダウンロード用のスレッドを生成してたので、そりゃものすごい回数のスレッドの生成・破棄が発生していた・・固定スレッドのプールといえど、ないよりはずっとましgood

まぁ、実際のアプリで使う場合は他の人が作った高機能なOmniThreadLibraryAsyncCalls とか使った方がいいと思うが、いちいちクレジットの表記とかライセンス絡みがだるすぎ・・・

というか、Delphiが最低限こんな感じの仕組みなりを標準で用意してくれりゃ、前に作ったDelphi用のWindows Liveライブラリ に、非同期バージョンのメソッドILiveClient.EnumerateResourcesAsync(...): IAsyncOperation<IEnumerable<IResource>>とか追加してみたいんだが・・

ダウンロードはSkyDriveから

Delphi 2010以降が必要です。

« UCDViewer 1.2.0リリース | トップページ | Delphiで非同期プログラミング・・・おまけ »

Delphi」カテゴリの記事

コメント

自作の画像ビューワに、こちらの非同期スレッドを組み込んで見ましたところ、非常に高速に動作して
感動しているところです。ありがとうございます。

ところで、拙作の画像ビューワですが、フォルダ単位でタブ表示するようにしているのですが、
連続でフォルダを選択して複数のタブが表示された場合、最初に選択されたタブのサムネイルが
表示しきるまで、次のタブのスレッドが動かないようなのですが、並行して動かすにはどうしたらよいでしょうか?

とりあえず、タブごとにTAsyncGroupを作ったりしてみましたが、関係ないようですね・・・(^^;

ご教示いただけると、幸いです。

>フォルダ単位でタブ表示するようにしているのですが、
>連続でフォルダを選択して複数のタブが表示された場合、最初に選択されたタブのサムネイルが

あー。これは、非同期で実行する処理をTThreadPool.RunAsyncに渡してると思いますが、内部ではスレッドプールというものが1つしかなくて、渡された順番に実行していく
作りになっています。ので、先に渡した処理が終わるまで後の処理の実行が開始されないので、ご指摘通りの動作になってしまっています。内部のスレッドプールは確か8個ワーカスレッドからなるので、内部で同時に8スレッドで並列処理を行ってますが。

複数のスレッドプールを持てるようにThreading.Async.pasを修正しないと対応できませんね。すみません。


代替案として、
1.ブログにも書きましたが、高機能なOmniThreadLibraryを使う(そもそも。使い方調べないといけませんし、タブごとにスレッドプールを割り当てたり今回のケースに対応できるのかわかりませんが)
2.Delphi XE7もってるなら似たようなパラレルライブラリが追加されてるので、調べてみる。(これもこ今回のケースに対応できるのかわかりませんが)。
3.フォルダを選択してタブを表示するときに、フォルダーに例えば、1000個の画像ファイルが含まれてるなら、1度に1000個のファイルを処理するのではなく、画面に表示されるファイルだけをTThreadPool.RunAsyncで処理して、スクロールで次のファイル群が表示されるたびに、TThreadPool.RunAsyncで処理を起動してごまかす。

まぁ、上のブログで、Tumblrビューワーの画面をのっけてますが、Saraさんの画像ビューワと似た感じでタブでどんどん開けるのですが、3.で書いたような感じで作ってましたね。

4.初心者の型には厳しいけど、Threading.Async自体が600行くらいなので修正してみる・・

つか、今ソースみたら、自分がやれば書き換えは30分ぐらいで終わると思うので、
本当に必要なら土日あたりにやってもいいかなと。

ご連絡 ありがとうございます。m(_ _)m

今現在の作りとして、フォルダーを選択した際にループにてフォルダー内のファイルを逐次
RunASyncに、入れるようにしています。また、GRIDのOnDrawCellで表示する画像がないものも
RunASyncに放り込むようにしています。
ですので、最初のループ処理を無効にしてOnDrawCellでの画像読み込みだけを有効にすると、
希望する動作に近いものになるのは、確認しています。
でも、やっぱり他のファイルも裏で読みたいなと(^^;

現状のままでは、対応できないのですね。残念です。 ちなみに修正の予定は?(^^;

ここは、TParallel.For を試してみるしかないか・・・

うまくいったら、(うまくいかなくても・・・)また書き込みに来ます。(^^;

おっと、書いている間に、さらに書き込みが・・・

>つか、今ソースみたら、自分がやれば書き換えは30分ぐらいで終わると思うので、
> 本当に必要なら土日あたりにやってもいいかなと。

できれば、お願いしたく・・・m(_ _)m

初心者というほど、Delphiとの付き合いは短くないですが・・・(Delphi 1から付き合っている・・・)
所詮は、趣味の域をでないので・・・難解なソースを読み解くほどの知識がない・・・(^^;

まぁ、時間をかけて いじり倒すことで、解決することもありますが・・・

あの後、冷静になって考えたんですが、よく考えたらキャンセルするのも1つの手じゃないですかね?

>とりあえず、タブごとにTAsyncGroupを作ったりしてみましたが、関係ないようですね・・・(
タブごとにTAsyncGroupを作って、タブが切り替わる時に、選択解除されるタブの非同期処理をキャンセル。また、再びタブが選択されたら再実行。そうすれば、アクティブなタブの非同期処理の実行開始の遅延が極力抑えられると。
そのためのTAsyncGroupでグループ化してました。
どのような事を非同期で処理してるかわかりませんが、キャンセルしたくない処理があればそれは、別のTAyncGroupで管理してと。

キャンセルする場合例えば、TAsyncGroupにこんなメソッドを追加すれば
TAsyncGroupで管理してるすべての非同期操作をキャンセルできます。
procedure TAsyncGroup.RequestCancel;
var
AsyncInfo: IAsyncInfo;
begin
FCriticalSection.Acquire;
try
for AsyncInfo in FAsyncInfos do
if AsyncInfo.Status <> TAsyncStatus.Created then
AsyncInfo.Cancel;
finally
FCriticalSection.Release;
end;
end;


>でも、やっぱり他のファイルも裏で読みたいなと(^^;
最初はタブごとにスレッドプールを割り当てればいいかなと思いましたが、CPUのコア数は限られてますし、どのみち処理に「優先順位」をつけないかぎり、自分の望む結果は得られないかなと。
仮にタブごとにスレッドプールを割り当てても、優先順位をつけないと、今度はタブをたくさん開いたときに、選択されてないタブにもCPUリソースが割り当てられるので、選択されてるタブの表示が遅くなるって事になると思います。
先ほどの例はキャンセルする事によって優先順位をつけましたが。

でも、とりあえず、スレッドプールを複数作れるように修正してみました。
修正前のThreading.Async.pasのソースを覗けば、TFixedThreadPoolクラスというのがTThreadPoolクラスの内部で宣言されて、外部から触ることができませんが、ただ、これを外側に出して自分でインスタンス化できるようにしただけです。
デフォルトのスレッドプールにアクセスするには、
TFixedThreadPool.Default.RunAsync(ほにゃらら)で。
また、TFixedThreadPool.Create(ワーカースレッド数)で自分でスレッドプールを
作れます。
上のOneDriveのフォルダにThreading.AsyncEx.pasという名でアップロードしてあります。


>ここは、TParallel.For を試してみるしかないか・・・
つか、Delphi XE7もってるんですか、うらやましい。XE7もってるならパラレルライブラリを使った方がいいと思いますね。自分のはあくまで、Delphiにパラレルライブラリが導入される前に作ったやつなので。
スレッドプールクラスに関しては自分のと似た同じやつがありますね。
http://docwiki.embarcadero.com/Libraries/XE7/ja/System.Threading.TThreadPool
まぁ、このスレッドプールはデストラクタを呼んで解放するとき、実行されてないタスクとかあったらどういう動きするのかなどソースみないとわかりませんが。

後は既にやってるのかもしれませんが、サムネイルの生成などを非同期で実行してるのなら、なるべくキャッシュするとかですかね。

素晴らしい、対応の速さに感謝、感謝です。(^^)

動作については、これから確認をしてみます。m(_ _)m

TParallel.Forに関しては、Web上での資料が少なくて、手を付けていませんでした・・・

って言うか、TParallel.Forの存在を知る前にこちらの記事が目に留まり作成に入ったので・・・

しかも、リモートデバッグの環境がないので、せっかく安定動作しているのを変えたくなかった・・(^^;

マルチスレッド処理を通常デバッガで強引に調査すると、かなりの確率でIDEごとフリーズするし・・・(^^;

TParallel.Forの方がお薦めとのことであれば、今回の修正を適用させてみたあとで、プロジェクトを
コピーして、比較してみたいと思います。

サムネイルのキャッシュについては、現在のプロジェクトにはまだ組み込んでいませんが、
最終的にはSQLite3を使ってDB化する予定です。

と、いうか、今作っているソフトの前身にあたるバージョンがあるわけですが、それがSQLite3で動作しています。
今回は完全リニューアル版としてDelphiXE7で作りなおしているところです・・・
XE8が既に発売されましたけどね・・・半年に1回は早すぎだろう・・・

現在、XE8リチャージ+アップデートサブスクリプションの加入を検討中です。(^^;

恥ずかしながら・・・ 使い方が解りませんでした・・・(^^;

もともとの部分がTFixedThreadPool.Default.RunAsyncに置き換えることで、今まで通りの動きと
いうのは、解りますが・・・

TFixedThreadPool.Createの方が・・・

pool := TFixedThreadPool.Create(8);
AsyncInfo := pool.RunAsync(・・・

とかすると、1つ目のフォルダーはうまくいきますが、2つ目を選択した瞬間にフリーズします・・・

そもそも、何か勘違いしていますかね・・・(^^;

>pool := TFixedThreadPool.Create(8);
>AsyncInfo := pool.RunAsync(・・・
使い方はこれであってますね。こっちではちゃんと動きます。
フリーズって反応返ってこなくなるってことですかね。

ちょっと簡単なデモアプリ作ってます。

>使い方はこれであってますね。こっちではちゃんと動きます。
>フリーズって反応返ってこなくなるってことですかね。
2つ目のフォルダーを押した瞬間に、まったく反応がなくなります。

タスクマネージャー上では、「応答なし」となっています。

タブごとにグループを分けてあるのが、問題ですかねぇ・・・

>ちょっと簡単なデモアプリ作ってます。
お手数をおかけいたします。m(_ _)m

Threading.AsycUtils.pasに問題になりそうな部分があったので修正しておきました。
Async.zipを新しくしておきました。


>タブごとにグループを分けてあるのが、問題ですかねぇ・・・
タブごとにスレッドプールを割り当てさらに、タブごとにAsyncGroupでグループ分けってことですかね??応答なしになる前にひょっとして、
AsyncGroupかTFixedThreadPoolのFreeかShutdownを呼んでたりします?

AsyncTest.zipを上げておきました。
一応適当に、画像ビューワっぽい感じで。
「フォルダを開く」ボタンを押してフォルダを選択すると、そのタブが開きます。
で、そのフォルダ内のjpgファイルを列挙して、サムネイルを非同期で生成します。
こんな感じでTFixedThreadPoolやAsyncGroup使うかなという例です。
リストビューはカスタムドローしてますが、ちょっと使い方があやしいかもしれません。

どうも、お手数をおかけしております。

>AsyncGroupかTFixedThreadPoolのFreeかShutdownを呼んでたりします?

いいえ、呼んでいません。

もちろん、タブをクローズするタイミングでは、呼ばれますが・・・

で、とりあえず、タブごとにグループを分けていたのを、元に戻してみましたが、
状況は変わりませんでした。

強引にデバッガで確認をしてみたところ、2つ目のフォルダを選択した直後にAccess Violationが
発生します。、ブレイクするとTComponent.Notificationのwhileループ内でブレイクされます。
これを無視して継続すると、TFixedThreadPool.TAsyncInfo.Execute内のExceptのelseにてカーソルが止まります。

何か解決につながるヒントになりますでしょうか?

リモートデバッガではないので、信頼性がどこまであるかは不明ですが・・・

>AsyncTest.zipを上げておきました。
お手数をおかけしております。

確認をさせていただきます。m(_ _)m

非同期操作で注意したいのは、解放したオブジェクトにアクセスしない事ですかね。特に呼ばれるタイミングがわかりずらいので。で、TAsyncGroupの役割は主にそのためのものですね。
上のブログで例では、
procedure TForm1.FormDestroy(Sender: TObject);
begin
FAsyncGroup.Shutdown;
FAsyncGroup.WaitForTermination; // Gracefullにすべての非同期操作が完了してから、終了
FAsyncGroup.Free;
end;
OnDestroyイベントですべての非同期操作が完了するまえで待機してますけど、これは、Button1のクリックハンドラから実行する非同期操作内で
TThread.Synchronize(nil,
procedure
begin
ListBox1.Items.Add('非同期操作完了')
end);
としてListBox1にアクセスしてるからです。非同期操作が完了する前にフォームを閉じてしまうと、ListBox1のインスタンスなどが破棄され、その後に、非同期操作が完了して、ListBox1.Items.Add('非同期操作完了')が実行されたらアクセスバイオレーションです。
ここらへんをもっと楽にできないかということで、色々思考錯誤してTAsyncGroupを用意したわけです。
もっといい方法があるかもしれませんが。

>で、とりあえず、タブごとにグループを分けていたのを、元に戻してみましたが、
>状況は変わりませんでした。
AsyncTest.zipの方では、タブごとにスレッドプールとグループを用意してますので、ほぼ同じ状況なので、何か違いが分かるかもしれません。
AsyncTest.zipの方で非同期で行う処理が速すぎると思うので、サムネイルの生成部分にあえいてウェイトをいれて、タブを更に開きまくったりしても、とりあえず、
アクセスバイオレーションとか応答なしになることは今のところ確認できませんね。

確認しました。

問題なく、動作しています。

作り的な違いとしては、サムネイルの作成だけではなく、画像の読み込み処理等も一緒に

非同期スレッドに含めていることでしょうか・・・

一連の流れとして、以下のような呼び出しになっています。

ファイル数分のループを行い、その時呼んでいるprocedureが非同期処理の本体になっていると・・・

そもそも、この使い方に問題があったようです・・・(^^;

for i := 0 to ShFileList.Items.Count - 1 do begin
abc(・・);
end;

procedure TForm1.abc(・・);
begin

Pool := TFixedThreadPool.Create(8);
AsyncInfo := Pool.RunAsync(
 ・ 画像読み込み処理
 ・ サムネイル作成処理
 ・ 各種情報をレコードに格納
 );

FAsyncGroup.Add(AsyncInfo);
AsyncInfo.Completed := AsyncCompleted;
AsyncInfo.Start;
end;

そこで、Poolの作成をループの外に出したところ、とりあえずフリーズはしなくなりました。
ちょっと、不安定ですが、動いています。まぁ不安定なのは、今後の調整で・・・
複数のフォルダーを選択してもある程度期待通りの動作をします。

Pool := TFixedThreadPool.Create(8);
for i := 0 to ShFileList.Items.Count - 1 do begin
abc(Pool,・・);
end;

procedure TForm1.abc(var Pool,・・);
begin

AsyncInfo := Pool.RunAsync(
 ・ 画像読み込み処理
 ・ サムネイル作成処理
 ・ 各種情報をレコードに格納
 );

FAsyncGroup.Add(AsyncInfo);
AsyncInfo.Completed := AsyncCompleted;
AsyncInfo.Start;
end;

処理の書き方として、こんな感じで大丈夫でしょうか?
(そもそも、不安定なのは、まだ記述に問題がある?)

とりあえず、Poolのフリーのタイミングがとりづらいので、やはりタブ毎にPoolを持たせたほうが

よさそうですね。それならば、タグの廃棄のタイミングでFreeすればよいし。

なんとか、解決のめどがつきました。ありがとうございます。m(_ _)m

もう少しいじって、安定動作するようになったら、結果を報告に来ます。

>for i := 0 to ShFileList.Items.Count - 1 do begin
>abc(・・);
>end;


このループってフォルダを1個選択して、そのフォルダに100個ファイル含まれてると100回(ShFileList.Items.Count=100)まわるってことですかね?てことで100個プール作成?
そうだとするとちょっとやりすぎですね。
スレッドプールというのは、そもそも必要なスレッドの生成・破棄のオーバーヘッドが大きいので、スレッドをプールして使い回そうというものなので、基本、スレッドプール自体はそんな頻繁に作ったり・破棄したりするものじゃなく、長い間使うものです。
だから、最初みたくよほどの事がない限りアプリ全体で1個とか、AsyncTest.zipのようにタブごとに1個と。ファイルごとにスレッドプール作るとかは画像ビューワの場合やりすぎですね。

>処理の書き方として、こんな感じで大丈夫でしょうか?
大丈夫ですね。

>とりあえず、Poolのフリーのタイミングがとりづらいので、やはりタブ毎にPoolを持たせたほうがよさそうですね。それならば、タグの廃棄のタイミングでFreeすればよいし。
タブ毎でいいと思います。
ファイル毎にプールは非常にやりすぎで、逆に、リソースの無駄遣いになります。

>スレッドプールというのは、そもそも必要なスレッドの生成・破棄のオーバーヘッドが大きいので、
>スレッドをプールして使い回そうというものなので、基本、スレッドプール自体はそんな頻繁に作ったり・
>破棄したりするものじゃなく、長い間使うものです。
ThreadPoolのPoolという文字をまったく気にしていませんでした・・・(^^;

なので、通常のThreadと同じ扱いで、バンバンCreateすれば良いのだろうと・・(^^; 

AsyncTest.zipは大変参考になりました。 あれがなければ、未だに気づいていなかった可能性も・・・(^^;

動作も、安定してきましたので、一安心というところです。

また、何かのおりに書込みをさせていただくこともあるかと思います。

その際は、懲りずにお付き合いいただけるとありがたいです。(^^;

本当にありがとうございました。m(_ _)m

コメントを書く

(ウェブ上には掲載しません)

トラックバック

この記事のトラックバックURL:
http://app.f.cocolog-nifty.com/t/trackback/1497665/44169271

この記事へのトラックバック一覧です: Delphiで非同期プログラミング:

« UCDViewer 1.2.0リリース | トップページ | Delphiで非同期プログラミング・・・おまけ »

自作ソフトウェア

無料ブログはココログ

メモ