スレッドを所有していないため、SerialPort読み取りでエラーが発生する
-
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のバインディングは素晴らしかったです。コード。