none
元々ある処理を一切変えずに非同期にしたい RRS feed

  • 質問

  • 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のところで同様のエラーが発生してしまいます。
    非同期に処理させる方法について大して理解していないので、非同期にさせる呼び出し方が間違っているのかもしれません・・・。
    重い処理のコードを一切変更することなく、待機レイヤーを描画させることはどのようにコードすると可能になるでしょうか?

    2015年9月28日 4: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
    
    #ですが、やはり佐祐理さんが仰っているようにリファクタリングするのが良いと私も思います。
    2015年9月28日 4:48
    モデレータ

すべての返信

  • GUIは特定のスレッドに関連付けられています。ですのでGUI処理に関しては非同期、つまり他のスレッドで実行することはできません。

    GUI処理と他の処理(DB処理など)が分離されていないのであれば、非同期は無理です。

    手が出せないほどの複雑化したコードは、いつまでたっても手が出せないままでしょうから、こつこつとリファクタリングしておくことをお勧めします。重い処理と思っていたものが実は無駄な処理だった、なんてことはよくあります。

    2015年9月28日 4:45
  • こんにちは。

    コントロールの操作は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
    
    #ですが、やはり佐祐理さんが仰っているようにリファクタリングするのが良いと私も思います。
    2015年9月28日 4:48
    モデレータ
  • 元々の重い処理が、スレッドに一切依存しないような処理であれば、デリゲートで元の処理を呼び出すところで非同期に実行する、というような方法で実現は可能です。

    しかし、スレッドに依存していたり、画面を操作している場合は単純にそういう方法は無理です。

    どうしても元の処理側に手を入れる必要が出てきます。

    もうひとつは、待機レイヤを別スレッドで、新しいメッセージポンプを生成して実行する方法ですが、まあどんどん難易度が高くなっていきますし、元フォームとの統合は多分できなくなっていきますね。

    2015年9月28日 5:22
  • 待機レイヤーはメインスレッドで、検索部分などの重い処理のみを別スレッドにするのが普通だと思います。

    やっぱりそうですよね。
    スレッドの向きというかが逆転してしまって、どうにもこうにも出来ない感じになっています。

    その重い処理の部分のみを別スレッドにしようとすればできるかもしれませんが、仰る通りInvoke()のしまくりで大改修になりそうですし。

    諦めて、くるくる回らず、「検索中」とか出るものにでも変更します。(出すだけならできそうなので)
    ありがとうございます。

    2015年9月28日 5:26
  • ご回答ありがとうございます。

    やはり難しいようですね。
    リファクタリングしようにも、仕様書ない、作った本人もなぜそうしたか覚えてないというよくある混沌とした状態なので、手をつけようにも怖くてつけられないというものです。。。
    それに加えて、事情により、こつこつやってるヒマがないというのもありますが・・・。

    諦めてちょっと別な表現に変えようと思います。ありがとうございました。
    2015年9月28日 5:30
  • takiru さま よろしく。

    仮に、技術的に、非同期の実装が可能になったとして、
    非同期にした為に起こり得る、データ処理の整合性を維持する為に、
    更に、元コードを弄る必要性が出て来ませんか。
    つまり、元々ある処理を一切変えず には、成し得ない気がするのですが。

    単純に、カーソルを WaitCursor にしたり、ラベルや文字やパネルを表示したりするのでは、対応出来ないのでしょうか。
    処理中であることを表現しよう と言う趣旨なら、これで充分な気がするのですが。

    元のコードはそのままに、最初から、別途、組み直した方が、目的に適いそうですが。

    2015年9月28日 5:39
  • リファクタリングしようにも、仕様書ない、作った本人もなぜそうしたか覚えてないというよくある混沌とした状態なので、手をつけようにも怖くてつけられないというものです。。。
    それに加えて、事情により、こつこつやってるヒマがないというのもありますが・・・。

    そうであれば、元々、非同期化も無理な話かと。ShiroYuki_Motさんも指摘されていますが、非同期に変更すれば実行するタイミング・スレッドが異なってきますから、当然エラーの発生個所やエラー処理タイミングも異なってきます。

    非同期に関わらず、何等か変更を加えておきながら今までと同じ動作を期待するというのは不可能な話です。

    2015年9月28日 5:50
  • 仮に、技術的に、非同期の実装が可能になったとして、

    非同期にした為に起こり得る、データ処理の整合性を維持する為に、
    更に、元コードを弄る必要性が出て来ませんか。
    つまり、元々ある処理を一切変えず には、成し得ない気がするのですが。

    単純に、カーソルを WaitCursor にしたり、ラベルや文字やパネルを表示したりするのでは、対応出来ないのでしょうか。
    処理中であることを表現しよう と言う趣旨なら、これで充分な気がするのですが。

    元コードをいじる必要はありますが、重い処理のメソッドの呼び元で制御してしまえば、重い処理自体には何ら影響を及ぼさずできるかな、と思った次第です。

    現状、WaitCursorすら適用していませんでした。
    やはり実現は難しいようなので、仰る通り、WaitCursor、ラベルあたりで対応しようかなと思っています。

    上記方法論はすぐ思いついてはいましたが、もうちょっとなんか色々できないかなと思いまして。
    (しかも色々やってたら、アニメーションgifはサイズ変更できなくて、どんな大きさの領域にも適用できるかと問われたらできなさそうでした。)

    2015年9月28日 7:05