提高 C# 串口性能






4.84/5 (20投票s)
本文介绍了一系列简单的测试程序,旨在演示 .NET 串行端口接口的性能问题以及如何改进它们。
目录
- 引言
- 硬件注意事项
- 时间读取器项目
- 消息定义
- 远程时间读取器 Mark 1
- 本地时间读取器 Mark 1
- 首次测试结果
- 远程时间读取器 Mark 2
- 第二次测试结果
- 本地时间读取器 Mark 2
- 第三次测试结果
- 结论
引言
在开发 *Station Blue* 产品过程中,发现了 .NET 串行端口接口的一些性能问题。*Station Blue* 产品软件运行在桌面和移动设备上,使用有线串行端口或蓝牙。所有情况下的端口速度均为 9600 波特,考虑到较新的数字模型铁路系统的实际数据速度限制在 9000 波特以下,这足以控制模型火车。然而,尽管平均数据流速率可能被描述为轻量级,但可能会发生大量数据突发。在这种情况下,发现基于 Microchip PIC 的远程设备实际上可以超越 PC 端 C#.NET 软件。
本文介绍了一系列简单的测试程序,旨在演示 .NET 串行端口接口的性能问题以及如何改进它们。在大多数实际情况下,串行端口用于方便 PC 控制设备。但是,为了避免不必要的复杂性,本文设计的测试设置在串行链路两端都使用了 PC。
硬件注意事项
如今,许多新计算机已不再配备物理串行端口。但是,如果您有此需求,主板上通常会有串行端口的连接点。因此,如果您喜欢拆开计算机机箱,您可能还有一个装满旧计算机零件的储藏室,其中一些零件可以相对容易地用于安装物理串行端口。如果不行,可以在电子商店购买 USB 转串行转换线。
无论如何,串行端口通信主要用于方便控制设备。在本项目中,两个计算机通过其串行端口连接在一起。为了使每个计算机都表现得像另一个设备的设备,需要在两台计算机之间使用零调制解调器电缆,或者将零调制解调器设备插入连接两台计算机的电缆中。
或者,可以使用蓝牙建立串行端口连接。对于尚未配备蓝牙的计算机,可以购买蓝牙加密狗,其价格与 USB 转串行转换线大致相同。但是,如果您认为蓝牙可以解决计算机/设备零调制解调器布线问题,请再考虑一下。典型的 *“我的蓝牙邻居”* 窗口包含代表提供服务的蓝牙设备的图标。因此,您的设备(或模拟设备)必须出现在该窗口中 PC 才能连接。因此,如果您的设备实际上是另一台计算机,您必须在该计算机的蓝牙服务窗口中确定哪些虚拟串行端口可用于提供所需的服务。
时间读取器项目
本文试图代表的典型场景是连接到提供用户友好界面的 PC 的数据收集设备。该用户友好界面已在 C# 中开发。它解释和显示收集到的数据,并可能需要根据多个输入将控制数据发送到设备。
如前所述,在本练习中,数据收集设备实际上是另一台 PC。收集的数据只是当天的时间。在提供界面的 PC 上,用户可以选择所需数据样本的数量和频率。收集到的数据样本显示在列表视图中, along with the time that the request for data was actually sent and the time that the data was finally received. 为了增加数据收集任务的复杂性,用户可能需要以不同的进程线程数量收集相同的样本。
在此项目中,数据收集设备称为“RemoteTimeReader”,用户友好界面称为“LocalTimeReader”。每种情况下的 Mark 1 版本都遵循相当常规的开发路径,但结果并不完美。Mark 2 版本提供了显著的改进。
消息定义
TimeReader 项目的消息定义在下表中说明
Message | 代码 | Data |
确认请求 | 0x01 | |
确认 | 0x02 | |
确认收到 | 0x03 | |
时间请求 | 0x04 | 索引,时间 1 |
时间 | 0x05 | 索引,时间 1,时间 2 |
设备(RemoteTimeReader)启动后,会定期发送确认请求消息。PC 界面(LocalTimeReader)启动后,会用确认消息响应确认请求消息。设备响应确认消息,发送确认收到消息。在长时间空闲期间,设备和接口都会通过发送确认请求消息和确认消息来测试线路。
值得注意的是,确认请求、确认和确认收到消息都只有一个字节长,以避免同步问题。实际上,当设备首次启动时,它只接受确认消息。在更大的系统中,如果检测到通信错误,设备可能会退回到仅发送确认请求消息并仅接收确认消息。
设备接收到的时间请求消息包含 PC 界面中发送消息的进程线程的索引以及消息发送时间。
发回 PC 界面的时间消息包含发送到设备的时间和索引信息,以及设备在远程端记录的实际时间。
那么,代码。Message
类用于收集消息数据并为后续进程定义该消息。它由 RemoteTimeReader 和 LocalTimeReader 使用。Message
类的构造函数如下所示
public Message(byte first)
{
code = first;
status = MessageStatus.InProgress;
if (code == 0x01 || code == 0x02 || code == 0x03)
status = MessageStatus.Complete;
else
dataCount = 0;
}
消息的第一个字节在构造函数中传递。第一个字节是代码 0x01、0x02 和 0x03 消息的唯一字节,因此对于这些消息,消息状态在构造函数中更改为“Complete”。对于长度大于一字节的消息,会调用 Add
方法,直到消息状态更改为“Complete”。Add
方法如下所示
public void Add(byte next)
{
if (code != 0x04 && code != 0x05)
return;
if (dataCount == 0)
{
index = next;
dataCount = 1;
return;
}
timeBytes[dataCount-1] = next;
dataCount++;
if (dataCount == 9 && code == 0x04)
{
status = MessageStatus.Complete;
localTime = (long)BitConverter.ToInt64(timeBytes, 0);
}
if (dataCount == 17 && code == 0x05)
{
status = MessageStatus.Complete;
localTime = (long)BitConverter.ToInt64(timeBytes, 0);
remoteTime = (long)BitConverter.ToInt64(timeBytes, 8);
}
}
时间以 tick 的形式发送和接收,每个 tick 长 8 字节。因此,代码 0x04 消息的数据部分长度为 9 字节,包括索引字节。带有两个时间值和一个索引字节,代码 0x05 消息的数据部分长度为 17 字节。一旦消息组装完毕,下游进程即可使用访问器 Code、Index、LocalTime 和 RemoteTime 获取收集到的值。
远程时间读取器 Mark 1
如前所述,RemoteTimeReader 程序旨在模拟收集时间值的远程设备。因此,其设计遵循极简主义路径。它是一个控制台应用程序,一旦 C# .NET SerialPort
类已初始化并设置好,就会在 Main
方法中的一个 while
循环中运行。此 while
循环将持续执行,只要“发送就绪”(CTS)线路保持高电平。CTS 线路是检查通信链路物理完整性的便捷方法。
while
循环的前几行如下所示
while (port.CtsHolding)
{
int totalBytes = port.BytesToRead;
if (totalBytes == 0)
{
Thread.Sleep(10);
if (sleepCount == 100)
{
if (!acknowledgementTimerElapsed())
return;
sleepCount = 0;
}
sleepCount++;
continue;
}
while
循环内的第一个命令检查端口以确定是否有任何数据可供读取。如果没有,程序将休眠 10 毫秒,递增一个计数器,然后重新开始循环。一旦休眠计数达到 100,表示已过 1 秒,则调用 acknowledgementTimerElapsed
方法。acknowledgementTimerElapsed
方法如下所示
static private bool acknowledgementTimerElapsed()
{
if (status == PortStatus.ConnectionEstablished)
status = PortStatus.ConnectionIdle;
else if (status == PortStatus.ConnectionIdle ||
status == PortStatus.NotConnected)
{
if (status == PortStatus.ConnectionIdle)
status = PortStatus.CheckConnected;
byte[] dataBytes = new byte[1];
dataBytes[0] = 0x01;
port.Write(dataBytes, 0, dataBytes.Length);
}
else if (status == PortStatus.CheckConnected)
{
Console.WriteLine("Communication lost - " +
"Timeout on acknowledge request");
return false;
}
return true;
}
此方法的行为主要由变量 status
决定。status
变量最初设置为 *Not Connected* 值,表示 RemoteTimeReader 尚未与 LocalTimeReader 建立连接。当 RemoteTimeReader 的状态为 *Not Connected* 时,acknowledgementTimerElapsed
方法将简单地每秒发送一次确认请求消息。
当收到消息时,ReceivedMessage
方法(稍后介绍)会将状态更改为 *ConnectionEstablished*。当调用 acknowledgementTimerElapsed
方法且状态为 *ConnectionEstablished* 时,它会将状态更改为 *ConnectionIdle*。因此,当再次调用 acknowledgementTimerElapsed
方法且状态仍为 *ConnectionIdle* 时,表示一秒钟内未收到任何消息。如果发生这种情况,状态将更改为 *CheckConnected* 并发送确认请求消息。
如果调用 acknowledgementTimerElapsed
方法且状态为 *CheckConnected*,则即使已发送确认请求消息,一秒钟内也未收到任何消息。如果发生这种情况,该方法将返回 false
,从而有效地关闭应用程序。
回到 Main
方法中的 while (port.CtsHolding)
循环,如果端口有数据可读,则使用前面介绍的 Message
类将该数据组装成消息。消息组装后,将调用 ReceivedMessage
方法(如下所示)。
static private void ReceivedMessage(Message receivedMessage)
{
if (receivedMessage.Code == 0x02)
{
status = PortStatus.ConnectionEstablished;
byte[] dataBytes = new byte[1];
dataBytes[0] = 0x03;
port.Write(dataBytes, 0, dataBytes.Length);
}
if (status == PortStatus.NotConnected)
return;
if (receivedMessage.Code == 0x04)
{
byte[] dataBytes = new byte[18];
dataBytes[0] = 0x05;
dataBytes[1] = receivedMessage.Index;
(BitConverter.GetBytes(receivedMessage.LocalTime)).CopyTo(dataBytes, 2);
(BitConverter.GetBytes(DateTime.Now.Ticks)).CopyTo(dataBytes, 10);
port.Write(dataBytes, 0, dataBytes.Length);
}
}
RemoteTimeReader 程序应只接收确认(代码 0x02)和时间请求(代码 0x04)消息。如果收到确认消息,它将通过发送确认收到消息进行响应。如果收到时间请求消息,它将用时间消息进行响应。此时间消息使用时间请求消息中提供的索引和时间以及当前机器的本地时间来构造。如果 status
变量设置为 *NotConnected*,则只接收确认消息。
本地时间读取器 Mark 1
LocalTimeReader 程序旨在成为一个基于 PC 的用户界面,用于显示远程设备提供的数据。因此,此端的串行端口代码的结构与 RemoteTimeReader 程序相当不同。ConnectedSerialPort
类继承了 .NET SerialPort
类,并为 SerialPort
类提供了所有必需的设置参数。LocalTimeReaderPort
类继承了 ConnectedSerialPort
类,并处理应用程序的特定细节。
在此端,没有无限循环;相反,ConnectedSerialPort
类设置了事件来处理 CTS 引脚的变化和数据接收。此外,在 LocalTimeReaderPort
类的构造函数中设置了一个一秒定时器,用于调用 acknowledgementTimerElapsed
方法,该方法与 RemoteTimeReader 程序中的 acknowledgementTimerElapsed
方法大致相同。
数据接收事件会导致调用 LocalTimeReaderPort
类中的 ProcessDataReceived
方法。ProcessDataReceived
方法如下所示
protected override void ProcessDataReceived(object sender, EventArgs e)
{
int totalBytes = this.BytesToRead;
if (totalBytes == 0)
return;
int byteCount = 0;
while (byteCount < totalBytes)
{
if (newMessage)
{
if (status != PortStatus.NotConnected)
status = PortStatus.ConnectionEstablished;
receivedMessage = new Message((byte)this.ReadByte());
if (receivedMessage.Status == Message.MessageStatus.InProgress)
newMessage = false;
else
ReceivedMessage(receivedMessage);
}
else
{
receivedMessage.Add((byte)this.ReadByte());
if (receivedMessage.Status != Message.MessageStatus.InProgress)
{
newMessage = true;
ReceivedMessage(receivedMessage);
}
}
byteCount++;
}
}
不出所料,当端口有数据可读时,此代码几乎与 RemoteTimeReader 程序中的代码相同。一旦组装完接收到的消息,就会调用 ReceivedMessage
方法。LocalTimeReaderPort
类中的 ReceivedMessage
方法如下所示
private void ReceivedMessage(Message receivedMessage)
{
if (receivedMessage.Code == 0x03)
status = PortStatus.ConnectionEstablished;
if (receivedMessage.Code == 0x01)
parent.ReceivedAcknowledgeRequest();
if (status == PortStatus.NotConnected)
return;
if (receivedMessage.Code == 0x05)
parent.ReceivedTime(receivedMessage.Index,
receivedMessage.LocalTime, receivedMessage.RemoteTime);
}
此方法当然补充了 RemoteTimeReader 程序中同名的方法。在这里,可能的接收消息是:确认请求(代码 0x01)、确认收到(代码 0x03)和时间(代码 0x05)。如果收到确认请求消息或时间消息,则会调用 MainForm
方法之一的 ReceivedAcknowledgeRequest
或 ReceivedTime
。
LocalTimeReader 界面如下所示
为了模拟大量数据流,可以通过使用标记为 *“数量”* 的数字增减控件并按“更新”来创建几个并发的数据收集线程。每个数据收集线程由一个包含列表视图的选项卡页表示。每个列表视图由四列组成。第一列只是一个序号。第二列是发送时间请求消息的时间。第三列是远程端收集的时间。第四列是接收到时间消息的时间。
用户界面还便于调整样本间隔和样本数量。一旦远程端的 RemoteTimeReader 启动,就按下“连接”。按下“运行”以开始数据收集。
实际处理每个选项卡页显示的 C# 代码包含在 ThreadPage
类中。ThreadPage
类当然继承了 .NET TabPage
类。当主窗体上的“运行”被按下时,将为每个选项卡页调用下面显示的 Run
方法。
public void Run(LocalTimeReaderPort port, int interval, int noTicks)
{
localTimeReaderPort = port;
tickTotal = noTicks;
intervalTimer.Interval = interval;
listView.Items.Clear();
tickCount = 0;
localTimeReaderPort.SendTimeRequest(index, DateTime.Now.Ticks);
if (tickTotal > 1)
intervalTimer.Enabled = true;
}
用于发送消息的 LocalTimeReaderPort
实例从主窗体传入。样本间隔和样本数量也传入,并用于设置间隔定时器。立即发送第一条时间请求消息。
间隔定时器的事件处理程序如下所示
void intervalTimer_Tick(object sender, EventArgs e)
{
if (localTimeReaderPort != null)
localTimeReaderPort.SendTimeRequest(index, DateTime.Now.Ticks);
if (tickCount == tickTotal)
{
intervalTimer.Enabled = false;
parent.RunStop();
}
tickCount++;
}
当然,每隔一段时间都会发送一条时间请求消息。一旦发送了所需数量的消息,定时器将被禁用,并调用 MainForm
类中的 RunStop
方法。
收到时间消息时,将调用下面显示的 ReceivedTime
方法。
public void ReceivedTime(long localTime, long remoteTime)
{
ListViewItem item = listView.Items.Add((listView.Items.Count+1).ToString());
if (localTime < DateTime.MinValue.Ticks || localTime > DateTime.MaxValue.Ticks)
item.SubItems.Add("Invalid Time");
else
item.SubItems.Add(GetTimeString(new DateTime(localTime)));
if (remoteTime < DateTime.MinValue.Ticks || remoteTime > DateTime.MaxValue.Ticks )
item.SubItems.Add("Invalid Time");
else
item.SubItems.Add(GetTimeString(new DateTime(remoteTime)));
item.SubItems.Add(GetTimeString(DateTime.Now));
}
此方法将接收到的数据 along with the current time 添加到 listview。
现在来看 MainForm
类。Update 按钮的事件处理程序调用下面显示的 RefreshTabPages
方法。
public void RefreshTabPages()
{
tabControl.Controls.Clear();
for (byte iii=0; iii < (byte)updateNumericUpDown.Value; iii++)
tabControl.Controls.Add(new ThreadPage(iii, this));
}
添加到 MainForm
类选项卡控件的 ThreadPage
类实例的数量由标记为 *“数量”* 的 updateNumericUpDown
控件确定。
Connect 按钮的事件处理程序如下所示
private void connectButton_Click(object sender, EventArgs e)
{
if (localTimeReaderPort != null)
localTimeReaderPort.ClosePort();
localTimeReaderPort = new LocalTimeReaderPort(this);
if (localTimeReaderPort.OpenPort(portComboBox.SelectedItem.ToString()))
{
connectButton.Enabled = false;
disconnectButton.Enabled = true;
}
else
MessageBox.Show("Port " + portComboBox.SelectedItem.ToString() + " failed to open");
}
按下“连接”后,将创建一个 LocalTimeReaderPort
类的新实例,然后打开串行端口。
收到确认请求消息时,将调用下面显示的 ReceivedAcknowledgeRequest
方法。
public void ReceivedAcknowledgeRequest()
{
if (this.InvokeRequired)
BeginInvoke((MethodInvoker)(delegate { ReceivedAcknowledgeRequest(); }));
else
{
runButton.Enabled = true;
localTimeReaderPort.SendAcknowledge();
}
}
收到确认请求消息时,“运行”按钮将被启用,并发送一条确认消息。请注意,调用了 BeginInvoke
方法将方法调用排队到 GUI 线程。使用 BeginInvoke
方法而不是 Invoke
方法意味着代码执行不会在调用完成之前挂起。
按下“运行”时,将调用每个 ThreadPage
类实例的 Run
方法。
收到时间消息时,将调用下面显示的 ReceivedTime
方法。
public void ReceivedTime(byte index, long localTime, long remoteTime)
{
if (this.InvokeRequired)
BeginInvoke((MethodInvoker)(
delegate { ReceivedTime(index, localTime, remoteTime); }));
else
{
((ThreadPage)tabControl.Controls[index]).ReceivedTime(localTime, remoteTime);
}
}
使用提供的索引调用适当的 ThreadPage
类实例的 ReceivedTime
方法。
首次测试结果
要进行测试设置,请先在远程计算机上运行 RemoteTimeReader 程序。如果要使用的串行端口是 COM1,则不需要输入参数。我更喜欢在命令提示符窗口中运行 RemoteTimeReader 等控制台应用程序,以便更容易阅读意外程序终止时产生的错误消息。
一旦 RemoteTimeReader 程序正在运行,就在本地计算机上启动 LocalTimeReader 程序并按“连接”。如果一切顺利,那么“运行”按钮应在几秒钟内启用。作为进一步检查,请使用一次采样和一个线程的默认值按“运行”,并观察测试结果是否出现在列表视图中。
一切看起来不错后,稍微增加一些难度。将线程数设置为 10,样本数设置为 50,样本间隔设置为 50,然后再次按“运行”。应得到与下图类似的结果。
可以做出以下观察
- *Local Time A* 值之间的间隔约为 110 毫秒,这比实际选择的 50 毫秒间隔长一倍多。
- *Remote Time* 值之间的间隔约为 188 毫秒。运行 10 个线程时,每个样本间隔发送 10 条长度为 10 字节的时间请求消息,并且串行端口的数据流速不到 9600 波特可用速度的一半。
- *Local Time B* 值之间的间隔约为 170 至 220 毫秒。
- *Local Time A* 和 *Local Time B* 之间的往返间隔最初约为 156 毫秒,相当快,但由于样本间隔比 *Remote Time* 间隔更快,最后的往返时间最终需要大约 5 秒。
尤其令人担忧的是 *Remote Time* 值之间的长间隔。不幸的是,根据收集到的信息,无法确定延迟是在本地端还是在远程端。
但是,有一点是肯定的,如果我们的 9600 波特线路上运行一个真实的数据收集设备,那么 LocalTimeReader 程序将不得不处理比我们这里看到的速度快两倍以上的数据。
远程时间读取器 Mark 2
为了提高远程端的速度,RemoteTimeReader 程序已用 C 语言重写。我称之为 C 而不是 C++,因为实际上没有定义类。Win32 API 需要一系列命令,如 CreateFile
、GetCommState
、SetCommState
、GetCommTimeouts
和 SetCommTimeouts
。此命令序列在 CodeProject 文章:Eshwar 编写的“使用 Win32 进行非重叠串行端口通信” 中有描述。
Eshwar 的代码和我的代码之间唯一显著的区别是,首先,不使用 SetCommMask
和 WaitCommEvent
命令。其次,读取超时设置为相当低的 1 秒。与 C# 版本一样,有一个主要的 while
循环。在 C 语言版本中,如下所示
while(true)
{
char buffer[1000];
DWORD readSize;
if (ReadFile(hSerialPort, buffer, 1000, &readSize, NULL) != 0)
{
.
.
.
.
}
if (!AcknowledgementTimerElapsed(hSerialPort))
break;
}
执行会在 ReadFile
命令处停止,直到收到数据或命令超时。由于读取超时设置为一秒,如果没有收到数据,则每秒执行一次,然后传递到 AcknowledgementTimerElapsed
方法。此 AcknowledgementTimerElapsed
方法基本上是 C# AcknowledgementTimerElapsed
方法的 C 版本。
如果收到数据,则执行 ReadFile
命令后的花括号内的代码。再次,解析和处理接收到的数据的操作与 C# 版本大致相同。
第二次测试结果
要测试新代码,请在远程计算机上运行 RemoteTimeReaderC 程序。一旦 RemoteTimeReaderC 程序运行,就在本地计算机上启动 LocalTimeReader 程序并按“连接”。
同样,当一切看起来不错后,稍微增加一些难度。将线程数设置为 10,样本数设置为 50,样本间隔设置为 50,然后按“运行”。应得到与下图类似的结果。
可以做出以下观察
- 与之前一样,*Local Time A* 值之间的间隔约为 110 毫秒,这比实际选择的 50 毫秒间隔长一倍多。
- 与之前一样,*Remote Time* 值之间的间隔约为 188 毫秒。
- 前四个 *Local Time B* 值都相同,然后这些值之间的间隔似乎稳定在 188 毫秒左右。
- 由于前四个 *Local Time B* 值都相同,*Local Time A* 和 *Local Time B* 之间的往返间隔一开始显得相当慢。然而,由于 *Local Time B* 值之间的间隔比之前更稳定在 188 毫秒,最后的往返时间保持不变。
如果这是一个简单的数据链路缓慢的问题,那么显而易见的解决方案是提高波特率或选择其他技术。然而,现实是我们的接口在处理以 9600 波特运行的设备时效果不佳。也许最重要的问题是您希望您的接口做什么?由于 GUI 的全部目的是与人类互动,它实际上不必那么快。
根据实践经验,我知道消息的捆绑(例如,可能同时到达四条消息)是一个真正的问题,因为可能需要捆绑中较早消息的信息来修改设备的行为。
本地时间读取器 Mark 2
由于两台计算机之间的数据流速仍然低于 9600 波特,下一步是完全断开 GUI 进程与串行端口进程的连接。这当然不会加快 GUI 的速度,但它会阻止可能在 GUI 相关进程中发生的捆绑。
为了实现断开连接,将以下变量添加到 LocalTimeReaderPort
类中。
private Queue queue1 = new Queue();
private Queue sendMessageQueue;
private Thread sendMessageThread;
private ManualResetEvent sendEvent = new ManualResetEvent(false);
private Queue queue2 = new Queue();
private Queue receiveMessageQueue;
private Thread receiveMessageThread;
private ManualResetEvent receivedEvent = new ManualResetEvent(false);
变量 sendMessageThread
是用于处理传出消息的进程线程,变量 queue1
和 sendMessageQueue
用于设置线程安全队列。同样,变量 receiveMessageQueue
是用于处理传入消息的进程线程,变量 queue2
和 receiveMessageQueue
用于设置线程安全队列。这些线程和队列在 LocalTimeReaderPort
类的构造函数中设置,如下所示
sendMessageThread = new Thread(this.SendMessageThread);
sendMessageQueue = Queue.Synchronized(queue1);
sendMessageThread.Start();
receiveMessageThread = new Thread(this.ReceiveMessageThread);
receiveMessageQueue = Queue.Synchronized(queue2);
receiveMessageThread.Start();
sendMessageThread
使用的 SendMessageThread
方法如下所示
private void SendMessageThread()
{
while (true)
{
while (sendMessageQueue.Count > 0)
{
SendMessage((byte[])sendMessageQueue.Dequeue());
}
sendEvent.WaitOne();
sendEvent.Reset();
}
}
调用 SendMessage
方法(与 LocalTimeReader Mark 1 版本相同),并将消息从发送队列中传递。然后执行等待 sendEvent
设置。当然,这意味着以前直接调用 SendMessage
方法的方法现在必须调用下面显示的 StartSendMessage
方法。
private void StartSendMessage(byte[] data)
{
sendMessageQueue.Enqueue(data);
sendEvent.Set();
}
新消息仅添加到队列,并设置 sendEvent
。
receiveMessageThread
使用的 ReceiveMessageThread
方法如下所示
private void ReceiveMessageThread()
{
while (true)
{
while (receiveMessageQueue.Count > 0)
{
ReceivedMessage((Message)receiveMessageQueue.Dequeue());
}
receivedEvent.WaitOne();
receivedEvent.Reset();
}
}
此方法与 SendMessageThread
非常相似。调用 ReceivedMessage
方法,并将消息从接收队列中传递。然后执行等待 receivedEvent
设置。当然,这意味着以前直接调用 ReceivedMessage
方法的 ProcessDataReceived
方法现在必须改调用下面显示的 StartReceivedMessage 方法。
private void ReceiveMessageThread()
{
while (true)
{
while (receiveMessageQueue.Count > 0)
{
ReceivedMessage((Message)receiveMessageQueue.Dequeue());
}
receivedEvent.WaitOne();
receivedEvent.Reset();
}
}
第三次测试结果
要测试新代码,请在远程计算机上运行 RemoteTimeReaderC 程序。一旦 RemoteTimeReaderC 程序运行,就在本地计算机上启动 LocalTimeReaderGT 程序并按“连接”。
同样,当一切看起来不错后,稍微增加一些难度。将线程数设置为 10,样本数设置为 50,样本间隔设置为 50,然后按“运行”。应得到与下图类似的结果。
可以做出以下观察
- 现在,*Local Time A* 值之间的间隔降至 60 毫秒,这非常接近实际选择的 50 毫秒间隔。
- *Remote Time* 值之间的间隔仍然约为 188 毫秒。
- 前两个 *Local Time B* 值之间的间隔相当高,然后这些值之间的间隔似乎稳定在 188 毫秒左右。
- 第一次,*Local Time A* 和 *Local Time B* 之间的往返间隔一开始显得相当慢,为 828 毫秒。最后一个往返值肯定很慢,大约需要 8 秒。但是,现在样本速率比以前快了近一倍,因此 8 秒的往返时间可能被认为是成比例的。
总的来说,速度提升不大。但是,采样率现在几乎是按时进行的,GUI 运行流畅,并以固定的间隔显示新结果。
结论
我理解 .NET SerialPort
类在有完整字节可供读取时会引发 DataReceived
事件。每个事件都会启动一个新的进程线程。但是,当所有可用的进程线程都在运行时,每个新接收到的数据字节都会被缓冲,直到有进程线程可用。这解释了在第二系列测试中看到的捆绑。
因此,似乎 LocalTimeReader 版本 1 中的某些进程在释放 DataReceived
事件所需的进程线程方面很慢。唯一的解决方案是最大限度地减少 DataReceived
事件处理程序所需的时间,将其任务缩减为仅将数据添加到队列。
为了使 GUI 运行流畅,似乎还需要一个出站数据队列。