试图跨线程数据绑定的线程安全问题

时间:2014-03-24 11:51:19

标签: vb.net multithreading data-binding datagrid thread-safety

我有一个带有datagrids的表单,该表单绑定到Data的绑定列表,该列表由来自tcpip连接的常量数据流填充。 TCPIP连接位于应该循环的线程上,当它找到足够的数据时,创建类Data的实例并将其添加到BindingList(Data)。我收到一条错误,上面写着“跨线程操作无效:控制''从其创建的线程以外的线程访问。”。堆栈跟踪指向将新数据添加到(数据的)绑定列表

的行
DataList.Insert(0, dataItem)

我觉得奇怪的是它继续正常运行并填充我的数据网格。我对多线程编程不是很熟悉,而且我的大部分代码都是异步的。我有一个数据锁和一些互斥(我刚学到的东西,但我不确定我是否正确使用)。我在某处读到使用事件会有所帮助,但如果不需要NewData事件,我可以摆脱它。任何有助于我的代码线程安全的帮助将非常感激。

表单上的绑定是这样的:

myDataGrid.datasource = myDataReader.DataList

这是开始阅读和阅读数据的课程(抱歉,我知道这是很多代码,但我不想留下一些可能很重要的内容):

Public Class DataReader
    Implements INotifyPropertyChanged
#Region "Properties"
Dim dataMutex As Mutex
Dim UnparsedMutex As Mutex
Dim mIP_Address As String
Dim convertingData As Boolean = False
Public Property IP_Address() As String
    Get
        Return mIP_Address
    End Get
    Set(ByVal value As String)
        mIP_Address = value
    End Set
End Property
Dim mPort As Integer
Public Property Port As Integer
    Get
        Return mPort
    End Get
    Set(ByVal value As Integer)
        mPort = value
    End Set
End Property
Private WithEvents mDataList As BindingList(Of Data)
Public ReadOnly Property DataList As BindingList(Of Data)
    Get
        Return mDataList
    End Get
End Property
Public Event valueChanged As Eventhandler
Private Event NewDataAvaiable(ByVal newData As BindingList(Of Data))
Private mUnparsedData As String = ""
Private Property UnParsedData As String
    Get
        Return mUnparsedData
    End Get
    Set(ByVal value As String)
        mUnparsedData = value
    End Set
End Property
#End Region
#Region "Private Variables"
Dim mTCPIPClient As TcpClient
Dim mConnected As Boolean
Dim mTCPIPStream As NetworkStream
Dim mLastError As String
Dim mDataThread As System.Threading.Thread
Private dataLock As New Object
#End Region
#Region "Constructors"
Public Sub New(ByVal IPAddress As String, ByVal Port As Integer)
    dataMutex = New Mutex(False, "MUTEXDATA")
    UnparsedMutex = New Mutex(False, "MUTEXUNPARSEDDATA")
    mIP_Address = IPAddress
    mPort = Port
    mConnected = False
    mDataList = New BindingList(Of Data)
    DataList.RaiseListChangedEvents = True
End Sub
#End Region
#Region "Events"
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Protected Sub OnPropertyChanged(ByVal name As String)
    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
End Sub
#End Region
#Region "Methods"
Public Sub ConnectTCPIP()
    Try
        If Not mTCPIPClient Is Nothing AndAlso mTCPIPClient.Connected Then
            mLastError = "Connection Error:Already connected"
            Exit Sub
        ElseIf String.IsNullOrEmpty(mIP_Address) Then
            mLastError = "Connection Error:No IP Address"
            Exit Sub
        End If
        DataList.Clear()
        UnParsedData = String.Empty

        mTCPIPClient = New TcpClient()
        mTCPIPClient.Connect(mIP_Address, mPort)
        If mTCPIPClient.Connected Then
            mTCPIPClient.ReceiveTimeout = 500
            mTCPIPClient.SendTimeout = 500
            mTCPIPClient.LingerState = New System.Net.Sockets.LingerOption(False, 0)
            mTCPIPClient.ReceiveBufferSize = 100000
            mTCPIPClient.SendBufferSize = 100000
            mTCPIPStream = mTCPIPClient.GetStream
            LaunchDataThread()
            If mDataThread.IsAlive Then mConnected = True
        Else
            mLastError = "Connection Error:Unknown Reason"
            Exit Sub
        End If
    Catch ex As Exception
        MessageBox.Show(ex.Message & ex.StackTrace)
    End Try
End Sub
Public Sub Disconnect()
    DisconnectTCPIP()
    KillDataThread()
End Sub
Private Sub DisconnectTCPIP()
    If Not mTCPIPClient Is Nothing AndAlso mTCPIPClient.Connected Then
        mTCPIPClient.Close()
        mTCPIPClient = Nothing
    End If
End Sub
Public Function IsConnected() As Boolean
    If mTCPIPClient Is Nothing OrElse mDataThread Is Nothing Then Return False
    Return mTCPIPClient.Connected AndAlso mDataThread.IsAlive
End Function
Public Sub LaunchDataThread()
    mDataThread = New System.Threading.Thread(AddressOf ReadData)
    mDataThread.Start()
End Sub
Public Sub KillDataThread()
    If Not mDataThread Is Nothing AndAlso mDataThread.IsAlive() Then
        mDataThread.Abort()
    End If
    mDataThread = Nothing
End Sub
Public Sub ReadData()
    Try
        While True
            Dim count As Short = 0
            While Not mTCPIPClient.Connected AndAlso count <= 5
                System.Threading.Thread.Sleep(5000)
                ConnectTCPIP()
                count += 1
            End While

            If mTCPIPClient.Connected = False Then Exit Sub
            Dim dataBuffer(500) As Byte
            Dim readBytes As Integer = 0
            UnparsedMutex.WaitOne()
            Do While mTCPIPStream.DataAvailable
                readBytes = mTCPIPStream.Read(dataBuffer, 0, 500)
                UnParsedData += System.Text.Encoding.ASCII.GetString(dataBuffer.Take(readBytes).ToArray())
            Loop
            UnparsedMutex.ReleaseMutex()
            If UnParsedData.Length > 0 Then ProcessNewData()
            System.Threading.Thread.Sleep(500)
        End While

    Catch ex As Exception
        Windows.Forms.MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace)
    End Try
End Sub
Public Sub ProcessNewData()
    Dim dataToProcess As String = ""
    UnparsedMutex.WaitOne()
    If String.IsNullOrEmpty(UnParsedData) Then
        UnparsedMutex.ReleaseMutex()
        Exit Sub
    End If
    dataToProcess = UnParsedData.Substring(0, UnParsedData.LastIndexOf("PLC") + 3)
    UnParsedData = UnParsedData.Remove(0, dataToProcess.Length)
    UnparsedMutex.ReleaseMutex()
    If Not String.IsNullOrEmpty(dataToProcess) Then
        Dim matches = dataToProcess.Split(";"c)

        If matches.Count > 0 Then
            Dim newData As New BindingList(Of Data)
            Try
                For i = 0 To matches.Count - 1
                    newData.Insert(0, New Data(matches(i).Value))
                Next
                RaiseEvent NewDataAvaiable(newData)
            Catch ex As Exception
                Windows.Forms.MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace)
            End Try
        End If
    End If
End Sub
Private Sub OnNewData(ByVal newData As BindingList(Of Data)) Handles Me.NewDataAvaiable
    SyncLock dataLock
        dataMutex.WaitOne()
        For Each dataItem As Data In newData
            DataList.Insert(0, dataItem)
            If DataList.Count > 5000 Then DataList.RemoveAt(5000)
        Next
        dataMutex.ReleaseMutex()
    End SyncLock
End Sub
#End Region
End Class

UPDATE :从不需要从UI更新bindingList(of Data),因此更改数据的唯一线程应该是DataReader类中的mDatathread。

1 个答案:

答案 0 :(得分:0)

查看InvokeRequired,Invoke,Delegate。

这里的示例,我在后台线程(后台工作者)中有一个UDP服务器,在主线程中接收MessageEntries和Listview1。这是NET4.0上的遗留winforms项目

从后台线程调用以下内容:

Delegate Sub SetListViewCallback(ByVal NewMsg As MessageEntry)

Private Sub addItemToListView(ByVal NewMsg As MessageEntry)
    Dim item1 As New ListViewItem(NewMsg.Time.ToString(), 0)
    item1.SubItems.Add(NewMsg.IP.ToString())
    If ListView1.InvokeRequired Then
        Dim d As New SetListViewCallback(AddressOf addItemToListView)
        Me.Invoke(d, New Object() {NewMsg})
    Else
        ListView1.Items.Add(item1)
    End If
End Sub

编辑:我刚用BindedList检查了这个方法作为DataGridView的数据源,它仍然有效。唯一的区别是条件if检查DataGridView对象是否需要Invoke,而在Else部分中你修改了BindedList。