- PR -

子スレッドから親スレッドのコントロールにスレッドセーフにアクセスするには・・・

1
投稿者投稿内容
McLaren
ぬし
会議室デビュー日: 2002/01/15
投稿数: 784
お住まい・勤務地: 東京
投稿日時: 2008-12-01 15:43
 お世話になっております。VB2005を最近始めました初心者です。
UDPを別スレッドで受信するプログラムを作成しております。

 下記プログラムで

@ここは成功する

の部分ではエミディエイトウィンドウにUDPで外部から送信した文字列がサクサク表示され、大変うまくいっておりますが、

Aここはエラーになる

の行のコメントを外して実行すると、

'System.InvalidOperationException' の初回例外が System.Windows.Forms.dll で発生しました。
有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'lblRcv' がアクセスされました。

とエラーが出ます。

 どうすれば、別スレッドのラベルコントロールにスレッドセーフにアクセスできますでしょうか。。
お手数おかけいたしますが、是非方法ご教授いただきたいと思います。



[ソリューション]**************************************
├[clsSocket]ユーザークラスプロジェクト
│  └clsUdpReceive.vb
└[Project1]メインのプロジェクト(参照設定で上のクラスを追加)
   └frmGraph.vb


[frmGraph.vb]**************************************
'※フォームにはlblRcvというラベルコントロールが一つだけ貼ってあります。
コード:

Imports System.Net.Sockets
Imports System.Net
Imports System.Threading
Imports System.Text

Public Class frmGraph
Private objCfgPcr As frmCfgPcr
Private objCfgEnv As frmCfgEnv

Public objLblRcv As Label

Private WithEvents UDP As New clsSocket.clsUdpReceive

'Load
Private Sub frmGraph_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
objLblRcv = Me.lblRcv
'Udpをマルチスレッドで受信する
UDP.Open(6800, "192.168.1.10", 6800)
End Sub

'Before form close
Private Sub frmGraph_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
UDP.Close()
End Sub

'Menu
Private Sub tsPcrCfg_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles tsPcrCfg.Click
'対象フォームのインスタンスが存在しなければ新規生成
If objCfgPcr Is Nothing OrElse objCfgPcr.IsDisposed() Then
objCfgPcr = New frmCfgPcr
End If
'フォームを可視化
objCfgPcr.Show()
End Sub
Private Sub tsEnvCfg_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles tsEnvCfg.Click
'対象フォームのインスタンスが存在しなければ新規生成
If objCfgEnv Is Nothing OrElse objCfgEnv.IsDisposed() Then
objCfgEnv = New frmCfgEnv
End If
'フォームを可視化
objCfgEnv.Show()
End Sub

Private Sub UDP_DataArrival(ByVal sender As Object, ByVal e As clsSocket.DataArrivalEventArgs) Handles UDP.DataArrival
Trace.WriteLine(e.ReceiveData) '@ここは成功する
'Me.lblRcv.Text = e.ReceiveData 'Aここはエラーになる
End Sub
End Class




[clsUdpReceive.vb]**************************************


コード:

Imports System.Net.Sockets
Imports System.Net
Imports System.Threading
Imports System.Text

'イベントを定義するデリゲート
Public Delegate Sub DataArrivalEventHandler(ByVal sender As Object, ByVal e As DataArrivalEventArgs)

''' <summary>
''' UDP受信を別スレッドで行うクラス
''' </summary>
''' <remarks></remarks>
Public Class clsUdpReceive

'イベントの公開
Public Event DataArrival As DataArrivalEventHandler

Private UdpListenerThread As Thread
Private Udp As UdpClient

Private mintLocalPort As Integer
Private mintRemotePort As Integer
Private mstrRemoteIP As String

Private mstrRcvData As String

Public Sub New()
End Sub

''' <summary>
''' UDPローカルポートを指定。
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks>Open()を呼ぶ前に指定しておく必要があります。</remarks>
Public Property LocalPort() As Integer
Get
Return mintLocalPort
End Get
Set(ByVal value As Integer)
mintLocalPort = value
End Set
End Property
''' <summary>
''' UDPリモートポートを指定。
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks>Open()を呼ぶ前に指定しておく必要があります。</remarks>
Public Property RemotePort() As Integer
Get
Return mintRemotePort
End Get
Set(ByVal value As Integer)
mintRemotePort = value
End Set
End Property
''' <summary>
''' UDPリモートアドレスを文字列で指定します。
''' 例)RemoteIP = "192.168.1.10"
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property RemoteIP() As String
Get
Return mstrRemoteIP
End Get
Set(ByVal value As String)
mstrRemoteIP = value
End Set
End Property

Public ReadOnly Property ReceiveData() As String
Get
Return mstrRcvData
End Get
End Property

''' <summary>
''' UDP接続をマルチスレッドで生成します。
''' </summary>
''' <remarks></remarks>
Public Sub Open()
'Udpをマルチスレッドで受信する
UdpListenerThread = New Thread(AddressOf UDPListener)
UdpListenerThread.IsBackground = True
UdpListenerThread.Start()
End Sub

''' <summary>
''' 引数付きでUDP接続をマルチスレッドで生成します。
''' プロパティを事前にセットしてOpen()を呼ぶのと同じです。
''' </summary>
''' <param name="LocalPort">LocalPortプロパティ参照</param>
''' <param name="RemoteIP">RemoteIPプロパティ参照</param>
''' <param name="RemotePort">RemotePortプロパティ参照</param>
''' <remarks></remarks>
Public Sub Open(ByVal LocalPort As Integer, ByVal RemoteIP As String, ByVal RemotePort As Integer)
mintLocalPort = LocalPort
mstrRemoteIP = RemoteIP
mintRemotePort = RemotePort
Me.Open()
End Sub

''' <summary>
''' UDP接続を閉じて生成された受信スレッドを終了します。
''' </summary>
''' <remarks></remarks>
Public Sub Close()
'UDP接続を閉じる
If Udp IsNot Nothing Then
'受信スレッドは例外で終了する
Udp.Close()
End If
'受信スレッド終了の確認
If UdpListenerThread IsNot Nothing AndAlso Not UdpListenerThread.Join(1000) Then
'終了が確認できないときは強制終了
UdpListenerThread.Abort()
Debug.Print("受信スレッド強制終了")
End If
End Sub

'Tread function
'引数はオブジェクト型のみ
Private Sub UDPListener(ByVal obj As Object)
Dim remoteAdd As IPAddress = IPAddress.Parse(mstrRemoteIP)
Dim remoteEP As New Net.IPEndPoint(remoteAdd, RemotePort)

'udp bind
Udp = New UdpClient(LocalPort)

Try
Do
Dim rcvBytes As Byte() = Udp.Receive(remoteEP)
mstrRcvData = Encoding.ASCII.GetString(rcvBytes)

'イベントを起こす
Dim e As New DataArrivalEventArgs(mstrRcvData)
RaiseEvent DataArrival(Me, e)
If e.Cancel Then
Exit Sub
End If

Loop
Catch ex As Exception
Debug.WriteLine(ex.Message)
If Udp IsNot Nothing Then
Udp.Close()
End If
Finally
Trace.WriteLine("UDP受信スレッド終了")
End Try

End Sub

Protected Overrides Sub Finalize()
MyBase.Finalize()
End Sub
End Class

''' <summary>
''' DataArrivalイベント引数
''' </summary>
''' <remarks></remarks>
Public Class DataArrivalEventArgs
Inherits System.ComponentModel.CancelEventArgs

Private mstrRcvData As String

Public Sub New(ByVal RcvData As String)
mstrRcvData = RcvData
End Sub

Public ReadOnly Property ReceiveData() As String
Get
Return mstrRcvData
End Get
End Property

End Class



[ メッセージ編集済み 編集者: McLaren 編集日時 2008-12-01 16:56 ]
セラフ
ベテラン
会議室デビュー日: 2005/12/01
投稿数: 95
お住まい・勤務地: 東北の顔の形といえば
投稿日時: 2008-12-01 16:33
こんにちは。

UDP受信スレッドとフォームのスレッドは別スレッドですが、
デリゲートを使うと簡単にアクセスできますよ。

↓この辺みてみてください。
http://msdn.microsoft.com/ja-jp/library/cc708906.aspx
http://codezine.jp/article/detail/139?p=1

※ソースは「BBコード」の「CODE タグ」で書いていただけると見やすくなります。
McLaren
ぬし
会議室デビュー日: 2002/01/15
投稿数: 784
お住まい・勤務地: 東京
投稿日時: 2008-12-01 17:18
セラフ 様
早速のご教授ありがとうございます。

 私が見ていたサイト
よりもご紹介くださったサイトのほうがわかりやすかったです。。

 ちなみに私が質問前に探しましたサイトは
http://msdn.microsoft.com/ja-jp/library/ms171728(VS.80).aspx
です。。


 ご紹介いただいた
http://codezine.jp/article/detail/139?p=1#form
にもありますように

コード:
    'TextBox1に文字列を追加する
    Dim count As Integer = _
        CInt(Me.Invoke(dlg, New Object() {threadName}))



のようにすればうまくいきそうな雰囲気が漂っていたのですが、
いざクラス(clsUdpReceive.vb)として実装しようとすると

「Me.」って書けない・・・

となりました。

 子スレッドの生成元のフォーム(frmGraph.vb)の参照を
clsUdpReceiveクラスをNewするときに登録するプロパティを
clsUdpReceiveに用意して、「Me.」の部分を置き換えるとできるのかと思って

System.Windows.Forms.Form 型

のプロパティを用意しようとすると、インテリセンスにも出てきませんし、
無理やり書いてもエディタにエラーが出て書けませんでした・・・

 困り果てて投稿させていただいた次第でございます。

Me.にとって替えてクラスに実装するにはどうすればよいでしょうか・・・
  



※[code]タグいい感じです・・・修正させていただきました。


 

セラフ
ベテラン
会議室デビュー日: 2005/12/01
投稿数: 95
お住まい・勤務地: 東北の顔の形といえば
投稿日時: 2008-12-01 18:01
おしい。そこは逆転の発想が必要です。
frmGraph側にデリゲートで実行するための、ラベルに文字列を書き出すだけのメソッドを書き、それをイベントの中からInvokeで呼び出します。

コード:

'Invokeメソッドで使用するデリゲート
Delegate Sub WriteLineDelegate(ByVal str As String)

private Sub WriteLine(ByVal str As String)
Me.lblRcv.Text = str
End Sub

Private Sub UDP_DataArrival(ByVal sender As Object, ByVal e As clsSocket.DataArrivalEventArgs) Handles UDP.DataArrival
Trace.WriteLine(e.ReceiveData) '@ここは成功する
'Me.lblRcv.Text = e.ReceiveData 'Aここはエラーになる

  'WriteLineDelegateの作成
Dim dlg As New WriteLineDelegate(AddressOf WriteLine)

'TextBox1に文字列を追加する
Me.Invoke(dlg, New Object() {e.ReceiveData})

End Sub




こんな感じでしょうか。実際に動かしてないから、バグッたらごめんなさい。
あとは、書き込みがバッティングしないように注意してください。

[ メッセージ編集済み 編集者: セラフ 編集日時 2008-12-01 21:58 ]
McLaren
ぬし
会議室デビュー日: 2002/01/15
投稿数: 784
お住まい・勤務地: 東京
投稿日時: 2008-12-01 18:27
セラフ 様

ありがとうございます。いけました!
おっしゃるとおりの方法でサクサク動いております!

お忙しいところご教授いただき、誠にありがとうございました!
風になる
ベテラン
会議室デビュー日: 2008/07/28
投稿数: 85
投稿日時: 2008-12-06 11:47
(利用規約違反のため削除いたしました。@ITクラブメンバーシップセンター)
1

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