- PR -

クラス内部で別スレッドからメインスレッドにマーシャリングする方法

投稿者投稿内容
kanai
ベテラン
会議室デビュー日: 2004/09/13
投稿数: 98
投稿日時: 2005-10-13 13:17
いつもお世話になっております。kanaiです。

時間のかかる処理をクラスの別スレッド上で実行し、その途中経過を
表示するWindowsアプリケーションを作成しております。

別スレッドを実行するクラスはフォームとは別で、途中経過はイベントを
通じて受け取るものとします。

このような場合、フォーム側でInvokeを使用してコントロールの操作が
メインスレッド上で行われるようにする必要がある、と理解していますが、
クラス側でInvoke相当の処理を行ってメインスレッドにマーシャリングすれば、
フォーム側は普通にイベントを処理するだけでよいのでは?と考えました。

もし可能ならば、クラスの利用者(フォーム側)はクラスが内部でスレッドを
使用しているかを意識する必要がなくなり、フォーム側の実装が容易になる
と思った次第です。

が、どのようにすれば実現できるのかわかりません。
コールバックデリゲート(参照元記事)を利用すれば可能かと思い試しましたが、
期待する結果にはなりませんでした。

そもそもこのようなことは可能なのでしょうか?

環境はWindows XP SP2 + VS.NET 2003(VB.NET)です。

サンプルコードは下記の通りです。

コード:
'Form1.vb
'デザイナでButton1とListBox1を貼り付けてください
Public Class Form1
    Inherits System.Windows.Forms.Form

#Region " Windows フォーム デザイナで生成されたコード "
    '省略
#End Region

    Private WithEvents c1 As New Class1

    Private Sub Button1_Click( _
        ByVal sender As System.Object, _
        ByVal e As System.EventArgs _
    ) Handles Button1.Click
        c1.Start()
    End Sub

    Private Sub c1_Progress( _
        ByVal sender As Object, _
        ByVal e As Class1.ProgressEventArgs _
    ) Handles c1.Progress
        Me.Invoke( _
            New AddListItemDelegate(AddressOf AddListItem), _
            New Object() {e.Message})
        'できれば、次のようにフォーム側ではInvokeをしなくてもよいようにしたい
        'AddListItem(e.Message)
    End Sub

    Private Delegate Sub AddListItemDelegate(ByVal message As String)

    Private Sub AddListItem(ByVal message As String)
        ListBox1.Items.Add(Threading.Thread.CurrentThread.Name & ":" & message)
    End Sub

End Class

'Class1.vb
Public Class Class1

    'スレッドオブジェクト
    Private _SubThread As Threading.Thread

    '別スレッドの処理を開始する
    Public Sub Start()
        'メインスレッドに名前を付ける
        Threading.Thread.CurrentThread.Name = "メインスレッド"
        '別スレッドを開始する
        _SubThread = New Threading.Thread( _
            New Threading.ThreadStart(AddressOf SubThreadProcess))
        _SubThread.Name = "サブスレッド"
        _SubThread.Start()
    End Sub

    '別スレッドで実行する処理
    Private Sub SubThreadProcess()
        '処理1
        Threading.Thread.Sleep(1000)
        OnProgress("処理1が終了しました。")     'この処理をメインスレッドで実行したい
        '処理2
        Threading.Thread.Sleep(1000)
        OnProgress("処理2が終了しました。")     'この処理をメインスレッドで実行したい
        '処理3
        Threading.Thread.Sleep(1000)
        OnProgress("処理3が終了しました。")     'この処理をメインスレッドで実行したい
    End Sub

    '処理の途中経過を報告するイベント
    Public Event Progress(ByVal sender As Object, ByVal e As ProgressEventArgs)

    'Progressイベントを発生させる
    Private Sub OnProgress(ByVal message As String)
        RaiseEvent Progress(Me, New ProgressEventArgs(message))
    End Sub

    'Progressイベントのイベント変数クラス
    Public Class ProgressEventArgs
        Inherits EventArgs

        Private _message As String

        Public ReadOnly Property Message() As String
            Get
                Return _message
            End Get
        End Property

        Public Sub New(ByVal message As String)
            _message = message
        End Sub

    End Class

End Class


なちゃ
ぬし
会議室デビュー日: 2003/06/11
投稿数: 872
投稿日時: 2005-10-13 13:49
引用:

kanaiさんの書き込み (2005-10-13 13:17) より:
このような場合、フォーム側でInvokeを使用してコントロールの操作が
メインスレッド上で行われるようにする必要がある、と理解していますが、
クラス側でInvoke相当の処理を行ってメインスレッドにマーシャリングすれば、
フォーム側は普通にイベントを処理するだけでよいのでは?と考えました。

もし可能ならば、クラスの利用者(フォーム側)はクラスが内部でスレッドを
使用しているかを意識する必要がなくなり、フォーム側の実装が容易になる
と思った次第です。


TimerやProcess等、イベントを持つコンポーネントを見てみるとヒントが見えてくるかもしれません。
※このようなときに、.NET Frameworkにおいて標準的と思われる構造に関して

SynchronizingObjectとかISynchronizeInvokeとかの説明を見るといいです。
必ずしもこれに合わせる必要はないですが。
まどか
ぬし
会議室デビュー日: 2005/09/06
投稿数: 372
お住まい・勤務地: ますのすし管区
投稿日時: 2005-10-13 14:14
引用:

コード:
Public Class Class1

    '別スレッドで実行する処理
    Private Sub SubThreadProcess()
        '処理1
        Threading.Thread.Sleep(1000)
        OnProgress("処理1が終了しました。")     'この処理をメインスレッドで実行したい
    End Sub

End Class





呼び出し側が処理方法を意識しない、つまりClass1が公開クラスという位置づけだと思います。
単にそれだけの問題なら、Class1に通知するインターフェースを実装すればよいのではないでしょうか。
Invokeうんぬんに関しては、別スレッドが直接呼ぶのかClass1が仲介して呼ぶのかに関わらず
呼び出し側がデリゲート参照を渡してあげればよいと思います。
kanai
ベテラン
会議室デビュー日: 2004/09/13
投稿数: 98
投稿日時: 2005-10-13 18:12

なちゃさん、まどかさんご回答いただきありがとうございます。

.NETの作法(?)に従い、クラス側にSynchronizingObject
プロパティを実装することで期待する動作を実現できました。

当初の目的としては、フォーム側から情報を渡さずに、クラス自身で
メインスレッドへのマーシャリングを行うことでしたが、この方法が
標準的、というのであればそれに従おうと思います。

フォーム側でデリゲートやInvokeを実装しなければならないのと比べて、
SynchronizingObject = Meを設定する程度であれば、(クラスの利用者
にとって)十分簡単、という判断もあります。

ところで、今回はじめて知ったのですが、ProcessやTimerは
デザイナに貼り付けると自動的に"SynchronizingObject = Me"が
設定されるようですね。これはやはり属性とかで定義されている
のでしょうか?
(自動的に設定したい、というわけではないのですが少し気になったので)

修正したサンプルコードは下記の通りです。

コード:
'Form1.vb
'デザイナでButton1とListBox1を貼り付けてください
Public Class Form1
    Inherits System.Windows.Forms.Form

#Region " Windows フォーム デザイナで生成されたコード "
    '省略
#End Region

    Private WithEvents c1 As New Class1

    Private Sub Button1_Click( _
        ByVal sender As System.Object, _
        ByVal e As System.EventArgs _
    ) Handles Button1.Click
        c1.SynchronizingObject = Me
        c1.Start()
    End Sub

    Private Sub c1_Progress( _
        ByVal sender As Object, _
        ByVal e As Class1.ProgressEventArgs _
    ) Handles c1.Progress
        'Me.Invoke( _
        '    New AddListItemDelegate(AddressOf AddListItem), _
        '    New Object() {e.Message})
        'できれば、次のようにフォーム側ではInvokeをしなくてもよいようにしたい
        AddListItem(e.Message)
    End Sub

    Private Delegate Sub AddListItemDelegate(ByVal message As String)

    Private Sub AddListItem(ByVal message As String)
        ListBox1.Items.Add(Threading.Thread.CurrentThread.Name & ":" & message)
    End Sub

End Class

'Class1.vb
Public Class Class1

    'スレッドオブジェクト
    Private _SubThread As Threading.Thread

    'マーシャリング用オブジェクト
    Private _SynchronizingObject As System.ComponentModel.ISynchronizeInvoke

    Public Property SynchronizingObject() As System.ComponentModel.ISynchronizeInvoke
        Get
            Return _SynchronizingObject
        End Get
        Set(ByVal Value As System.ComponentModel.ISynchronizeInvoke)
            _SynchronizingObject = Value
        End Set
    End Property

    '別スレッドの処理を開始する
    Public Sub Start()
        'メインスレッドに名前を付ける
        Threading.Thread.CurrentThread.Name = "メインスレッド"
        '別スレッドを開始する
        _SubThread = New Threading.Thread( _
            New Threading.ThreadStart(AddressOf SubThreadProcess))
        _SubThread.Name = "サブスレッド"
        _SubThread.Start()
    End Sub

    '別スレッドで実行する処理
    Private Sub SubThreadProcess()
        '処理1
        Threading.Thread.Sleep(1000)
        ReportProgress("処理1が終了しました。")     'この処理をメインスレッドで実行したい
        '処理2
        Threading.Thread.Sleep(1000)
        ReportProgress("処理2が終了しました。")     'この処理をメインスレッドで実行したい
        '処理3
        Threading.Thread.Sleep(1000)
        ReportProgress("処理3が終了しました。")     'この処理をメインスレッドで実行したい
    End Sub

    Private Sub ReportProgress(ByVal message As String)
        'SynchronizingObjectが設定されていればInvoke、そうでなければ普通に実行
        If Not _SynchronizingObject Is Nothing Then
            _SynchronizingObject.Invoke( _
                New OnProgressDelegate(AddressOf OnProgress), _
                New Object() {message})
        Else
            OnProgress(message)
        End If
    End Sub

    'OnProgressメソッド用のデリゲート
    Private Delegate Sub OnProgressDelegate(ByVal message As String)

    '処理の途中経過を報告するイベント
    Public Event Progress(ByVal sender As Object, ByVal e As ProgressEventArgs)

    'Progressイベントを発生させる
    Private Sub OnProgress(ByVal message As String)
        RaiseEvent Progress(Me, New ProgressEventArgs(message))
    End Sub

    'Progressイベントのイベント変数クラス
    Public Class ProgressEventArgs
        Inherits EventArgs

        Private _message As String

        Public ReadOnly Property Message() As String
            Get
                Return _message
            End Get
        End Property

        Public Sub New(ByVal message As String)
            _message = message
        End Sub

    End Class

End Class


まどか
ぬし
会議室デビュー日: 2005/09/06
投稿数: 372
お住まい・勤務地: ますのすし管区
投稿日時: 2005-10-13 18:43
引用:

SynchronizingObject = Meを設定する程度であれば、
コード:

    'OnProgressメソッド用のデリゲート
    Private Delegate Sub OnProgressDelegate(ByVal message As String)





私が今おこなっているのは、Delegateをスレッドクラス側でPublic定義し
受け取りオブジェクトとDelgate参照をプロパティにしています。
コード:
'Class MyTreadClass
Public Class MyCallBacksKind
    Public Delegate Sub Progress(ByVal Message As String)
End Class
Public Property Reciever() As Object
Public Property ProgressHandler() As MyCallBacksKind.Progress
--------------------------------------------------------------
'Caller
objMyThread = New MyThreadClass
With objMyThread
    .Reciever = Me
    .ProgressHandler = New MyCallBacksKind.Progress(AddressOf MyThread_Progress)
    .StartThread()
End With
Private Sub MyThread_Progress(ByVal Message As String)
End Sub


kanai
ベテラン
会議室デビュー日: 2004/09/13
投稿数: 98
投稿日時: 2005-10-13 20:14
まどかさん、ご回答ありがとうございます。

引用:

私が今おこなっているのは、Delegateをスレッドクラス側でPublic定義し
受け取りオブジェクトとDelgate参照をプロパティにしています。



フォーム(Caller=Form1)が、クラス(MyTreadClass=Class1)に対して
受け取りオブジェクトとデリゲート参照を渡す、という理解でよろしいでしょうか?

今回、「クラス利用者(フォーム側)の実装を簡単にしたい」と考えております。
となると、「このプロパティにデリゲートを設定ください」というのは、
クラス利用者にとって若干敷居が高くなってしまうと思います。

もしこの方法で、フォームから何も情報を渡さずにクラス内部でメインスレッド
にマーシャリングできるならば、ぜひ実装したいのですが・・・。

編集:BBコードの間違いを訂正

[ メッセージ編集済み 編集者: kanai 編集日時 2005-10-13 20:15 ]
渋木宏明(ひどり)
ぬし
会議室デビュー日: 2004/01/14
投稿数: 1155
お住まい・勤務地: 東京
投稿日時: 2005-10-14 00:05
本題とあまり関係が無くてごめんなさい。

引用:

このような場合、フォーム側でInvokeを使用してコントロールの操作が
メインスレッド上で行われるようにする必要がある、と理解していますが、
クラス側でInvoke相当の処理を行ってメインスレッドにマーシャリングすれば、



それって「マーシャリング」なんだっけ?
まどか
ぬし
会議室デビュー日: 2005/09/06
投稿数: 372
お住まい・勤務地: ますのすし管区
投稿日時: 2005-10-14 00:26
引用:

フォーム(Caller=Form1)が、クラス(MyTreadClass=Class1)に対して
受け取りオブジェクトとデリゲート参照を渡す、という理解でよろしいでしょうか?

今回、「クラス利用者(フォーム側)の実装を簡単にしたい」と考えております。
となると、「このプロパティにデリゲートを設定ください」というのは、
クラス利用者にとって若干敷居が高くなってしまうと思います。


イベントハンドラとデリゲートプロシージャを記述するのは同じ負担ですよね。
私の例はそれに加えてクラスへの明示的な割り当てが増える、位に私は思っています。
自動でプロシージャ定義が出てこないのは面倒ですけど。
まぁイベントのほうがぜんぜんわかりやすいインターフェースだとは思います。
#こっちのほうがいいというような話ではありませんので、念のため。

[quote]
それって「マーシャリング」なんだっけ?
[/quite]
私も気になってました。
あと、「フォーム側で」っていうのも。

スキルアップ/キャリアアップ(JOB@IT)