このパフォーマンスが低くひどいシリアル ポート コードを改善するにはどうすればよいでしょうか?
-
22-07-2019 - |
質問
非常に不安定な醜いシリアル ポート コードがあります。
void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
Thread.Sleep(100);
while (port.BytesToRead > 0)
{
var count = port.BytesToRead;
byte[] buffer = new byte[count];
var read = port.Read(buffer, 0, count);
if (DataEncapsulator != null)
buffer = DataEncapsulator.UnWrap(buffer);
var response = dataCollector.Collect(buffer);
if (response != null)
{
this.OnDataReceived(response);
}
Thread.Sleep(100);
}
}
Thread.Sleep(100) 呼び出しのいずれかを削除すると、コードは動作を停止します。
もちろん、これは本当に遅くなり、多くのデータがストリーミングすると、睡眠をさらに大きくしない限り、それも同様に機能しなくなります。(純粋なデッドロックのように動作を停止します)
データカプセクターとデータアコレクターはMEFが提供するコンポーネントですが、そのパフォーマンスは非常に優れています。
クラスには、データを受信するためのバックグラウンドワーカーを起動する聴取()メソッドがあります。
public void Listen(IDataCollector dataCollector)
{
this.dataCollector = dataCollector;
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += new DoWorkEventHandler(worker_DoWork);
worker.RunWorkerAsync();
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
port = new SerialPort();
//Event handlers
port.ReceivedBytesThreshold = 15;
port.DataReceived += new SerialDataReceivedEventHandler(port_DataReceived);
..... remainder of code ...
提案は大歓迎です!
アップデート:*IDataCollector クラスの機能について簡単にメモします。送信されたデータのすべてのバイトが単一の読み取り操作で読み取られるかどうかを知る方法はありません。したがって、データが読み取られるたびに、完全で有効なプロトコルメッセージが受信されたときにtrueを返すデータコールレクターに渡されます。この場合、ここでは、同期バイト、長さ、CRC、およびテールバイトをチェックします。実際の作業は、後で他のクラスによって行われます。*
更新 2:提案に従ってコードを置き換えましたが、まだ何か問題があります。
void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
var count = port.BytesToRead;
byte[] buffer = new byte[count];
var read = port.Read(buffer, 0, count);
if (DataEncapsulator != null)
buffer = DataEncapsulator.UnWrap(buffer);
var response = dataCollector.Collect(buffer);
if (response != null)
{
this.OnDataReceived(response);
}
}
これは、高速で安定した接続で正常に機能することがわかります。ただし、OnDataReceived はデータを受信するたびに呼び出されるわけではありません。(詳細については、MSDN ドキュメントを参照してください)。したがって、データが断片化され、イベントデータ内で1回しか読み取らない場合。
そして今、私は最初にループを持っていた理由を覚えています。なぜなら、接続が遅いか不安定な場合、実際に複数回読む必要があるからです。
明らかに while ループのソリューションに戻ることはできません。どうすればよいでしょうか?
解決
元の while ベースのコード部分に関して私が最初に懸念するのは、バイト バッファーにメモリが一定に割り当てられることです。ここに「new」ステートメントを置くと、特に .NET メモリ マネージャーにアクセスしてバッファにメモリを割り当て、同時に最後の反復で割り当てられたメモリを取得して、最終的なガベージ コレクションのために未使用のプールに送り返します。比較的タイトなループ内で行うには、非常に多くの作業が必要になるように思えます。
私が興味があるのは、設計時にこのバッファーを適切なサイズ (たとえば 8K) で作成し、メモリの割り当て、割り当て解除、断片化のすべてを行わなくて済むようにすることによって得られるパフォーマンスの向上についてです。それは役に立ちますか?
private byte[] buffer = new byte[8192];
void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
Thread.Sleep(100);
while (port.BytesToRead > 0)
{
var count = port.BytesToRead;
var read = port.Read(buffer, 0, count);
// ... more code
}
}
ループの反復ごとにこのバッファーを再割り当てすることに関するもう 1 つの懸念は、バッファーがすでに十分に大きい場合は再割り当てが不要になる可能性があることです。次のことを考慮してください。
- ループ反復 1:100バイトを受信しました。100バイトのバッファを割り当てる
- ループ反復 2:75バイトを受信しました。75バイトのバッファを割り当てる
このシナリオでは、ループ反復 1 で割り当てられた 100 バイトのバッファーは、ループ反復 2 で受信した 75 バイトを処理するには十分すぎるため、実際にはバッファーを再割り当てする必要はありません。100 バイトのバッファを破棄して 75 バイトのバッファを作成する必要はありません。(もちろん、バッファーを静的に作成してループから完全に移動するだけの場合、これは意味がありません。)
別の見方をすると、DataReceived ループはデータの受信のみに関係していると言えます。これらの MEF コンポーネントが何をしているのかはわかりませんが、それらの作業をデータ受信ループで実行する必要があるかどうかは疑問です。受信したデータを何らかのキューに入れて、MEF コンポーネントがそこでデータを取得できるようにすることは可能ですか?DataReceived ループをできるだけ高速に保つことに興味があります。おそらく、受信したデータをキューに入れて、すぐにさらなるデータの受信作業に戻ることができるでしょう。おそらく、別のスレッドをセットアップして、キューに到着するデータを監視し、MEF コンポーネントがそこからデータを取得して、そこから作業を実行できるようにすることができます。これによりコーディングが増える可能性がありますが、データ受信ループの応答性を可能な限り高めるのに役立つ可能性があります。
他のヒント
そして、それはとても簡単になります...
DataReceivedハンドラーを使用しますが、ループを使用せず、Sleep()を使用しない場合、準備ができているデータを読み取り、それをどこかに(キューまたはMemoryStreamに)プッシュします
または
スレッド(BgWorker)を開始し、(ブロッキング)serialPort1.Read(...)を実行し、再度、取得したデータをプッシュまたはアセンブルします。
編集:
あなたが投稿した内容から言うと、イベントハンドラをドロップし、Dowork()内のバイトを読み取るだけです。これには、ReadBufferSizeよりも(かなり)小さい限り、必要なデータ量を指定できるという利点があります。
Update2に関するEdit2:
イベントをまったく使用せずに、BgWorker内でwhileループを使用する方がはるかに優れています。簡単な方法:
byte[] buffer = new byte[128]; // 128 = (average) size of a record
while(port.IsOpen && ! worker.CancelationPending)
{
int count = port.Read(buffer, 0, 128);
// proccess count bytes
}
現在、レコードのサイズは可変であり、次の126バイトが入って1つのレコードが完了するのを待ちたくありません。これを調整するには、バッファーサイズを小さくするか、ReadTimeOutを設定します。非常にきめ細かくするには、port.ReadByte()を使用できます。 ReadBufferから読み取るため、実際には遅くなりません。
データをファイルに書き込みたいときにシリアルポートが頻繁に停止する場合、これは簡単な方法です。可能であれば、単一のファイルに入れる予定のすべてのバイトを保持するのに十分な大きさのバッファーを作成してください。次に、以下に示すように、datareceivedイベントハンドラーにコードを記述します。次に、機会があれば、その下に示すように、バッファ全体をファイルに書き込みます。シリアルポートがバッファーにTOを読み取っている間にバッファーからFROMを読み取る必要がある場合は、バッファーされたストリームオブジェクトを使用して、デッドロックや競合状態を回避してください。
private byte[] buffer = new byte[8192];
var index = 0;
void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
index += port.Read(buffer, index, port.BytesToRead);
}
void WriteDataToFile()
{
binaryWriter.Write(buffer, 0, index);
index = 0;
}