【问题标题】:SerialPort reading cause error because of not owning thread由于不拥有线程,SerialPort 读取导致错误
【发布时间】:2010-11-29 11:02:02
【问题描述】:

我有一个简单的 WPF Windows 应用程序尝试使用 System.IO.Ports.SerialPort 读取串行端口。

当我尝试读取 DataReceived 事件中的传入数据时,我收到一个异常,说我无权访问该线程。我该如何解决?

我在 WPF 窗口类中有这个:

Public WithEvents mSerialPort As New SerialPort()
Private Sub btnConnect_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnConnect.Click
    With mSerialPort
        If .IsOpen Then
            .Close()
        End If
        .BaudRate = 4800
        .PortName = SerialPort.GetPortNames()(0)
        .Parity = Parity.None
        .DataBits = 8
        .StopBits = StopBits.One
        .NewLine = vbCrLf

        .Open()
    End With
End Sub

Private Sub mSerialPort_DataReceived(ByVal sender As Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles mSerialPort.DataReceived
    If e.EventType = SerialData.Chars Then
        txtSerialOutput.Text += mSerialPort.ReadExisting()
    End If
End Sub

Protected Overrides Sub Finalize()
    If mSerialPort.IsOpen Then
        mSerialPort.Close()
    End If
    mSerialPort.Dispose()
    mSerialPort = Nothing
    MyBase.Finalize()
End Sub

当DataReceived 事件触发时,我在mSerialPort.ReadExisting() 上得到以下异常:

System.InvalidOperationException was unhandled
  Message="The calling thread cannot access this object because a different thread owns it."
  Source="WindowsBase"
  StackTrace:
       at System.Windows.Threading.Dispatcher.VerifyAccess()    at System.Windows.Threading.DispatcherObject.VerifyAccess()    at System.Windows.DependencyObject.GetValue(DependencyProperty dp)    at System.Windows.Controls.TextBox.get_Text()    at Serial.Serial.mSerialPort_DataReceived(Object sender, SerialDataReceivedEventArgs e) in D:\SubVersion\VisionLite\Serial\Serial.xaml.vb:line 24    at System.IO.Ports.SerialPort.CatchReceivedEvents(Object src, SerialDataReceivedEventArgs e)    at System.IO.Ports.SerialStream.EventLoopRunner.CallReceiveEvents(Object state)    at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)    at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)    at System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(_ThreadPoolWaitCallback tpWaitCallBack)    at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)

【问题讨论】:

    标签: .net wpf vb.net serial-port


    【解决方案1】:

    欢迎来到多线程的神奇世界!!!

    您的所有 UI 元素(类实例)只能由 UI 线程访问/更新。我不会详细介绍这个线程亲和力,但它是一个重要的主题,你应该检查一下。

    数据进入串行端口的事件发生在 与 UI 线程不同的线程上。 UI 线程有一个消息泵来处理 Windows 消息(如鼠标点击等)。您的串行端口不会发送 Windows 消息。当数据进入串行端口时,将使用与您的 UI 线程完全不同的线程来处理该消息。

    因此,在您的应用程序中,mSerialPort_DataReceived 方法在与您的 UI 线程不同的线程中执行。您可以使用线程调试窗口来验证这一点。

    当您尝试更新您的 UI 时,您正在尝试修改一个具有线程关联性的控件,该控件与来自不同线程的 UI 线程相关,这会引发您看到的异常。

    TL;DR:您正在尝试在 UI 线程之外修改 UI 元素。使用

    txtSerialOutput.Dispatcher.Invoke
    

    在 UI 线程上运行更新。 There's an example here on how to do thisin the community content of this page.

    Dispatcher 将在 UI 线程上调用您的方法(它向 UI 发送一条 windows 消息,说“Hai guize,运行此方法 kthx”),然后您的方法可以安全地从 UI 线程更新 UI。

    【讨论】:

    • 你知道Windows.Forms 场景(不是 WPF)对Dispatcher 对象的类似访问吗?
    • 所有 Windows 窗体控件都有一个名为 InvokeRequired 的属性和一个名为 Invoke 的方法。它们的作用与 Dispatcher 在 WPF 中的作用相同。只需用谷歌搜索这两个术语,您就会找到很多关于该主题的文档。
    【解决方案2】:

    根据Will 的回答,我现在已经解决了我的问题。我认为问题出在访问mSerialPort.ReadExisting(),而问题实际上是从DataReceived 事件中访问GUI 元素txtSerialOutput,该事件在单独的线程中运行。

    我添加了这个:

    Private mBuffer As String = ""
    Delegate Sub DelegateSetUiText()
    
    Private Sub UpdateUiFromBuffer()
        txtSerialOutput.Text = mBuffer
    End Sub
    

    ...我将DataReceived 事件更改为:

    Private Sub mSerialPort_DataReceived(ByVal sender As Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles mSerialPort.DataReceived
        If e.EventType = SerialData.Chars Then
            mBuffer += mSerialPort.ReadExisting()
            txtSerialOutput.Dispatcher.Invoke(New DelegateSetUiText(AddressOf UpdateUiFromBuffer))
        End If
    End Sub
    

    【讨论】:

      【解决方案3】:

      UI 只能由主应用程序线程更新。串行端口事件的异步回调在单独的线程上在后台处理。正如 Will 提到的,您可以使用 Dispatcher.Invoke 在 UI 线程上对 UI 组件属性更改进行排队。

      但是,由于您使用的是 WPF,因此有一个使用绑定的更优雅和惯用的解决方案。假设您在串行端口上接收到的数据对您的业务对象具有重要价值,您可以让 DataReceived 事件更新对象中的属性,然后将 UI 绑定到该属性。

      粗略代码:

      Public Class MySerialData
        Implements System.ComponentModel.INotifyPropertyChanged
        Public Event PropertyChanged(sender as Object, e as System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotfifyPropertyChanged.PropertyChanged
      
        private _serialData as String
        Public Property SerialData() As String
          Get
            Return _serialData
          End Get
          Set(value as String)
            If value <> _serialData Then
              _serialData = value
              RaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("SerialData"))
            End If
        End Property
      

      然后在您的 XAML 文件中,您可以将文本框绑定到此对象属性:

      <TextBox Text="{Binding Path=SerialData}"/>
      

      这假定 DataContext 设置为 MySerialData 类的实例。做这个额外的管道的好处是 WPF 现在将自动为你处理所有的跨线程封送处理,所以你不必担心哪个线程正在调用 UI 更改,WPF 中的绑定引擎只是让它工作。显然,如果这只是一个扔掉的项目,那么额外的前期代码可能不值得。但是,如果您正在执行大量异步通信并更新 UI,则 WPF 的此功能是一个真正的救星,并消除了多线程应用程序常见的一大类错误。我们在执行大量 TCP 通信的重线程应用程序中使用 WPF,并且 WPF 中的绑定非常好,尤其是当通过网络传输的数据意味着更新 UI 中的多个位置时,因为您不必在整个过程中进行线程检查你的代码。

      【讨论】:

      • 这个想法听起来很有希望。我试过了,但没有用。它编译了一个没有错误的运行,但文本框的内容没有更新。我回到了另一个可行的解决方案......我给了你一个+1的原则。它可能会在其他情况下工作......
      • 是的,第一次正确处理可能有点棘手,因为绑定只是在出现问题时静默失败。运行时,检查 Visual Studio 中的控制台输出,因为绑定错误将被注销到 VS 控制台。您可以在代码隐藏类或 XAML 的资源部分中设置 DataContext。感谢您的投票,希望您可以让它发挥作用,因为一旦您第一次完成它,它就真的很漂亮。
      猜你喜欢
      • 1970-01-01
      • 2011-07-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多