トップ回答者
元々ある処理を一切変えずに非同期にしたい

質問
-
Visual Basic 2012
.NET Franework 4.5プログラムのある一定処理について重くなってフリーズ現象みたくなってしまうため、処理中であることを表現しようと思っています。
対象箇所となる部分がかなり多くあること、コードが複雑化していることがあるため、元々あるプログラムの呼び出し部分以外に影響を与えず実装したいのですが、可能でしょうか?
以下のようなプログラムがもともとあります。Public Class Form ' F5を押したときの処理 Private Sub F5() DispData() End Sub ' DBを検索したりとかして画面に情報を出す処理 Private Sub DispData() ' DB検索とか ' フォーム上のコントロールを操作したりとか ' あれこれなんでもしまくる処理 End Sub End Class
ここで、DispData()の処理レスポンスが遅いので、待機レイヤーなるものを作りました。
待機レイヤーは以下のようなものです。
・指定したコントロールの領域を半透明に塗りつぶす。(フォーム全体を指定することも可能)(A)
・くるくる回るgif画像をコントロールの中心に表示する。(B)
・元フォーム.AddOwnedForm((A))、(A)フォーム.AddOwnerdForm((B))でフォームに乗せる。
・(A)で領域を求めるために、指定したコントロールのBounds.Locationなどから領域を求める。
・フォームの位置やサイズが変更された場合に描画し直すため、指定したコントロールからFindForm()によってフォームを捕捉。これらを制御する大元のクラス内容は以下のようなものです。
Imports System.Drawing ''' <summary> ''' 待機用レイヤーを提供します。 ''' </summary> ''' <remarks></remarks> Public Class WaitLayer Implements IDisposable Private _rendered As Boolean = False Private _disposed As Boolean = False Private _frame As WaitLayerFrame = Nothing Private _renderControl As Control = Nothing Private _ownerForm As Form = Nothing ''' <summary> ''' 待機レイヤーを表示後に呼び出す命令用デリゲートです。 ''' </summary> ''' <param name="args">引数群</param> ''' <remarks></remarks> Public Delegate Sub WaitingProcessDelegate(args() As Object) ''' <summary> ''' 待機用レイヤーを指定したコントロール上に表示します。 ''' </summary> ''' <param name="renderControl">レイヤーを表示したいコントロール</param> ''' <remarks></remarks> Public Sub Render(renderControl As Control) ' 描画済みの場合は一旦削除 If (Me._rendered) Then Me.Dispose() Me._disposed = False End If ' 親フォームを見つけ出す Dim frm As Form = Nothing If renderControl.GetType().IsSubclassOf(GetType(Form)) Then frm = CType(renderControl, Form) Else frm = renderControl.FindForm() End If ' オブジェクトの把握 Me._ownerForm = frm Me._renderControl = renderControl ' イベントの作成 AddHandler Me._ownerForm.Move, AddressOf Me._rerenderEvent AddHandler Me._ownerForm.Resize, AddressOf Me._rerenderEvent AddHandler Me._renderControl.DockChanged, AddressOf Me._rerenderEvent AddHandler Me._renderControl.Move, AddressOf Me._rerenderEvent AddHandler Me._renderControl.Resize, AddressOf Me._rerenderEvent ' フレームの作成 Me._frame = New WaitLayerFrame() Me._ownerForm.AddOwnedForm(Me._frame) Me._frame.Show() 'フレームの位置とサイズを設定 Me._setLayerLayout() ' (D) ' 描画済みであることを通知 Me._rendered = True ' (E) End Sub ''' <summary> ''' 待機用レイヤーを指定したコントロール上に表示し、指定した命令を実行します。 ''' </summary> ''' <param name="renderControl">レイヤーを表示したいコントロール</param> ''' <param name="dlgt">待機用レイヤーを表示後に呼び出す命令</param> ''' <param name="args">命令に引き渡す引数群</param> ''' <remarks></remarks> Public Sub RenderAndRun(renderControl As Control, dlgt As WaitingProcessDelegate, Optional args() As Object = Nothing) ' マウスポインタを待機に変更 Dim preCurosr As Cursor = Cursor.Current Cursor.Current = Cursors.WaitCursor ' 待機用レイヤーの表示 Me.Render(renderControl) ' (C) ' デリゲートの実行 dlgt(args) ' 待機用レイヤーの削除 Me.Dispose() ' マウスポインタの復帰 Cursor.Current = preCurosr End Sub ''' <summary> ''' フレームの位置・サイズを設定する。 ''' </summary> ''' <remarks></remarks> Private Sub _setLayerLayout() Dim framePoint As Point Dim width As Integer Dim height As Integer If (Me._renderControl.Parent Is Nothing) Then ' フォームの場合は、フォーム内部のみを描画するようにする If (Me._ownerForm Is Me._renderControl) Then framePoint = Me._ownerForm.PointToClient(Me._ownerForm.Bounds.Location) framePoint.X = Me._renderControl.Location.X + (framePoint.X * -1) framePoint.Y = Me._renderControl.Location.Y + (framePoint.Y * -1) width = Me._ownerForm.ClientRectangle.Width height = Me._ownerForm.ClientRectangle.Height Else ' 親がないって認識なのに、対象がフォームですらないのでエラー Throw New Exception("待機レイヤーの作成に失敗しました。ターゲットの指定に誤りがあります。") End If Else ' コントロールの領域、幅、高さ framePoint = Me._renderControl.Parent.PointToScreen(Me._renderControl.Bounds.Location) width = Me._renderControl.Width height = Me._renderControl.Height End If ' フレーム Me._frame.Width = width Me._frame.Height = height Me._frame.Left = framePoint.X Me._frame.Top = framePoint.Y ' フレーム内部のレイアウト設定 Me._frame.SetLayerLayout() End Sub ''' <summary> ''' 再レンダリングを必要とした際のイベント ''' </summary> ''' <param name="sender"></param> ''' <param name="e"></param> ''' <remarks></remarks> Private Sub _rerenderEvent(sender As Object, e As EventArgs) Me._setLayerLayout() End Sub ''' <summary> ''' 待機用レイヤーの削除を行います。 ''' </summary> ''' <remarks></remarks> Public Sub Dispose() Implements IDisposable.Dispose Me.Dispose(True) GC.SuppressFinalize(Me) End Sub ''' <summary> ''' 待機用レイヤーの削除が行われた際にイベントの削除とマネージドオブジェクトの解放を行う。 ''' </summary> ''' <param name="disposing"></param> ''' <remarks></remarks> Protected Overridable Sub Dispose(disposing As Boolean) If Me._disposed Then Return End If If disposing Then ' イベントの削除 RemoveHandler Me._ownerForm.Move, AddressOf Me._rerenderEvent RemoveHandler Me._ownerForm.Resize, AddressOf Me._rerenderEvent RemoveHandler Me._renderControl.DockChanged, AddressOf Me._rerenderEvent RemoveHandler Me._renderControl.Move, AddressOf Me._rerenderEvent RemoveHandler Me._renderControl.Resize, AddressOf Me._rerenderEvent ' 親フォームから削除 Me._frame.RemoveOwnedForm(Me._ownerForm) ' レイヤーの削除 Me._frame.Close() ' レンダリングが終了したことを通知 Me._rendered = False ' 強制的にガベージコレクト GC.Collect() End If Me._disposed = True End Sub End Class
呼び出し方は以下のようにしました。
' F5を押したときの処理 Private Sub F5() Dim wl As New WaitLayer() wl.RenderAndRun(JdSpr, AddressOf DispData) End Sub
基本的にはできているんですが、処理が重いため、gif画像が正しく描画されず、くるくる回りません。
そこで、待機レイヤー部分を別スレッドにしてしまおうかと思ったんですが、待機レイヤーを描画する処理の中で、元のフォームだのコントロールだののプロパティを取得しているせいか、(C)部分を呼び出すタイミングでエラーになってしまいます。
> 有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール '元のフォーム名' がアクセスされました。
因みに(D)、(E)部分をコメントアウトしても、End Subのところで同様のエラーが発生してしまいます。
非同期に処理させる方法について大して理解していないので、非同期にさせる呼び出し方が間違っているのかもしれません・・・。
重い処理のコードを一切変更することなく、待機レイヤーを描画させることはどのようにコードすると可能になるでしょうか?
回答
-
こんにちは。
コントロールの操作はUIスレッドから行う必要があります。
>待機レイヤー部分を別スレッドにしてしまおうかと思ったんですが
待機レイヤーはメインスレッドで、検索部分などの重い処理のみを別スレッドにするのが普通だと思います。
コントロールの更新が必要な場合はUIスレッドにInvokeしてやる必要があります。(追記)
以下は単純すぎる対応ですが、DispDataの中でバックグラウンドスレッドの部分とUI操作の部分を切り分けだけでもして良いのであれば
DispDataをAsyncにしてやれば、とりあえずの対応は出来るかもしれないです。Private Sub F5() DispData() End Sub 'Private Sub DispData() Private Async Sub DispData() ' DB検索とか ' フォーム上のコントロールを操作したりとか ' あれこれなんでもしまくる処理 'System.Threading.Thread.Sleep(1000) Await Task.Run(Sub() System.Threading.Thread.Sleep(1000) End Sub) Label1.Text = "1" 'System.Threading.Thread.Sleep(1000) Await Task.Run(Sub() System.Threading.Thread.Sleep(1000) End Sub) Label1.Text = "2" End Sub
#ですが、やはり佐祐理さんが仰っているようにリファクタリングするのが良いと私も思います。- 編集済み Tak1waMVP, Moderator 2015年9月28日 5:21
- 回答としてマーク takiru 2015年9月28日 5:37
すべての返信
-
こんにちは。
コントロールの操作はUIスレッドから行う必要があります。
>待機レイヤー部分を別スレッドにしてしまおうかと思ったんですが
待機レイヤーはメインスレッドで、検索部分などの重い処理のみを別スレッドにするのが普通だと思います。
コントロールの更新が必要な場合はUIスレッドにInvokeしてやる必要があります。(追記)
以下は単純すぎる対応ですが、DispDataの中でバックグラウンドスレッドの部分とUI操作の部分を切り分けだけでもして良いのであれば
DispDataをAsyncにしてやれば、とりあえずの対応は出来るかもしれないです。Private Sub F5() DispData() End Sub 'Private Sub DispData() Private Async Sub DispData() ' DB検索とか ' フォーム上のコントロールを操作したりとか ' あれこれなんでもしまくる処理 'System.Threading.Thread.Sleep(1000) Await Task.Run(Sub() System.Threading.Thread.Sleep(1000) End Sub) Label1.Text = "1" 'System.Threading.Thread.Sleep(1000) Await Task.Run(Sub() System.Threading.Thread.Sleep(1000) End Sub) Label1.Text = "2" End Sub
#ですが、やはり佐祐理さんが仰っているようにリファクタリングするのが良いと私も思います。- 編集済み Tak1waMVP, Moderator 2015年9月28日 5:21
- 回答としてマーク takiru 2015年9月28日 5:37
-
-
リファクタリングしようにも、仕様書ない、作った本人もなぜそうしたか覚えてないというよくある混沌とした状態なので、手をつけようにも怖くてつけられないというものです。。。
それに加えて、事情により、こつこつやってるヒマがないというのもありますが・・・。そうであれば、元々、非同期化も無理な話かと。ShiroYuki_Motさんも指摘されていますが、非同期に変更すれば実行するタイミング・スレッドが異なってきますから、当然エラーの発生個所やエラー処理タイミングも異なってきます。
非同期に関わらず、何等か変更を加えておきながら今までと同じ動作を期待するというのは不可能な話です。
-
仮に、技術的に、非同期の実装が可能になったとして、
非同期にした為に起こり得る、データ処理の整合性を維持する為に、
更に、元コードを弄る必要性が出て来ませんか。
つまり、元々ある処理を一切変えず には、成し得ない気がするのですが。
単純に、カーソルを WaitCursor にしたり、ラベルや文字やパネルを表示したりするのでは、対応出来ないのでしょうか。
処理中であることを表現しよう と言う趣旨なら、これで充分な気がするのですが。元コードをいじる必要はありますが、重い処理のメソッドの呼び元で制御してしまえば、重い処理自体には何ら影響を及ぼさずできるかな、と思った次第です。
現状、WaitCursorすら適用していませんでした。
やはり実現は難しいようなので、仰る通り、WaitCursor、ラベルあたりで対応しようかなと思っています。
上記方法論はすぐ思いついてはいましたが、もうちょっとなんか色々できないかなと思いまして。
(しかも色々やってたら、アニメーションgifはサイズ変更できなくて、どんな大きさの領域にも適用できるかと問われたらできなさそうでした。)