我有一个从串行读取数据的类,具有高阈值(1个字节) 我有一个变量存储来自串口的所有数据:_dataReceived。
private volatile string _dataReceived;
我正在使用DataReceived事件来存储这些数据,然后我开始一个动作来处理它。
private void _port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
string newData = _port.ReadExisting();
_dataReceived += newData;
new Action(() =>
{
Debug("Data received: {0}", newData);
ParseAnswers();
}).BeginInvoke(null, null);
}
处理它,包括从变量中删除它并处理答案。 ParseAnswers方法如下所示:
private void ParseAnswers()
{
string cmd = null;
int idx = -1;
lock (_dataReceived)
{
idx = _dataReceived.IndexOf(Environment.NewLine);
if (idx != -1)
{
cmd = _dataReceived.Substring(0, idx);
_dataReceived = _dataReceived.Substring(idx + 2);
}
else
return;
}
...
}
99.9%的时间都有效。
但有时我会在这一行得到一个ArgumentOutOfRangeException:
cmd = _dataReceived.Substring(0, idx);
现在,我的问题是: 我的变量是volatile,这意味着我总是访问真正的值而不是缓存。 我很确定我的DataReceived事件一直在增加(快速),但是我使用lock语句来阻止任何其他线程更改此值。 如果没有在字符串中使用NewLine,就无法运行这段代码(子字符串)。 并且这个IndexOf无法从字符串中返回索引。
所以......这里到底发生了什么事?
测试任何东西都非常困难,因为它只是每个月发生一次,但我们很欣赏任何有关实际情况的理论。
感谢您的任何建议!
答案 0 :(得分:6)
volatile 关键字永远不会解决同步问题。该错误清晰可见,您做了一个硬假设,即在ParseAnswers()消耗字符串并完成运行之前,DataReceived事件处理程序不会再次执行。这是一厢情愿的想法,当事件处理程序再次触发时,您的代码崩溃并在ParseAnswers()解析时替换字符串。使用 volatile 实际上使得更多可能会导致代码崩溃:)您还应该注意到数据丢失,当ParseAnswers()运行得太晚时会发生这种情况。
解决方案非常简单,给ParseAnswers()一个参数。传递字符串。
使用Invoke()而不是BeginInvoke()也是一个解决方案,SerialPort确保DataReceived在执行时不能被触发。但这很危险,当你试图调用Close()时,你的程序很容易死锁。
答案 1 :(得分:5)
这就是为什么我们建议在私有只读字段上使用锁。
dataReceived
方法中的_port_DataReceived
可能会被ParseAnswers
方法中的另一个线程更改(因为访问未同步),而lock(_dataReceived)
正在为前一个事件执行_dataReceived += newData;
。
所以会发生什么,现在有两个线程竞争private string _dataReceived;
private readonly object padLock = new object();
private void _port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
string newData = _port.ReadExisting();
lock (padLock)
{
_dataReceived += newData;
}
new Action(() =>
{
Debug("Data received: {0}", newData);
ParseAnswers();
}).BeginInvoke(null, null);
}
private void ParseAnswers()
{
...
lock (padLock)
{
idx = _dataReceived.IndexOf(Environment.NewLine);
if (idx != -1)
{
cmd = _dataReceived.Substring(0, idx);
_dataReceived = _dataReceived.Substring(idx + 2);
}
}
...
}
,它们都被允许,因为它们都使用了锁对象的不同引用。
记住锁定语句适用于引用,volatile
更改引用,因此可以自由地允许另一个线程进入关键区域(因为它现在使用不同的字符串实例)。
简单修复就是:
_dataReceived
请注意我已删除了_port_DataReceived
修饰符(这是多余的),并且我也同步了{{1}}方法中对{{1}}的访问权限(非常非常重要)