スレッドを所有していないため、SerialPort読み取りでエラーが発生する

StackOverflow https://stackoverflow.com/questions/1443944

  •  22-07-2019
  •  | 
  •  

質問

System.IO.Ports.SerialPort でシリアルポートを読み取ろうとする単純なWPF Windowsアプリケーションがあります。

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)
役に立ちましたか?

解決

マルチスレッドの魔法の世界にようこそ!!!

何が起こっているのかというと、すべてのUI要素(クラスインスタンス)にアクセス/更新できるのはUIスレッドだけです。このスレッドアフィニティの詳細については説明しませんが、これは重要なテーマであり、確認する必要があります。

シリアルポートでデータが着信するイベントは、 UIスレッドとは異なるスレッドで発生しています。 UIスレッドには、ウィンドウのメッセージ(マウスクリックなど)を処理するメッセージポンプがあります。シリアルポートはWindowsメッセージを送信しません。データがシリアルポートに到着すると、UIスレッドとはまったく異なるスレッドがそのメッセージの処理に使用されます。

したがって、アプリケーション内では、mSerialPort_DataReceivedメソッドはUIスレッドとは異なるスレッドで実行されています。これを確認するには、スレッドデバッグウィンドウを使用します。

UIを更新しようとすると、別のスレッドからUIスレッドのスレッドアフィニティを使用してコントロールを変更しようとしているため、表示された例外がスローされます。

TL; DR:UIスレッドの外部でUI要素を変更しようとしています。使用

txtSerialOutput.Dispatcher.Invoke

UIスレッドで更新を実行します。 このコミュニティコンテンツでこれを行う方法の例がありますページ。

DispatcherはUIスレッドでメソッドを呼び出し(「ハイguize、このメソッドkthx」を実行するというウィンドウメッセージをUIに送信します)、メソッドはUIスレッドから安全にUIを更新できます。

他のヒント

ウィル、問題を解決しました。問題は 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

UIは、メインアプリケーションスレッドによってのみ更新できます。シリアルポートイベントの非同期コールバックは、別のスレッドでバックグラウンドで処理されます。前述のとおり、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の変更を呼び出しているかを心配する必要はありません。作業。明らかに、これが単なるスローアウェイプロジェクトである場合、余分な事前コードの価値はないかもしれません。ただし、多くの非同期通信を行ってUIを更新している場合、WPFのこの機能は実際の節約になり、マルチスレッドアプリケーションに共通する大きなクラスのバグを排除します。大量のTCP通信を行う重度にスレッド化されたアプリケーションでWPFを使用します。特に、スレッドを介してデータがUIの複数の場所を更新することを意図している場合、WPFのバインディングは素晴らしかったです。コード。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top