易失性和锁定不起作用 - C#4.0

时间:2014-12-10 12:31:48

标签: c# multithreading serial-port locking volatile

我有一个从串行读取数据的类,具有高阈值(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无法从字符串中返回索引。

所以......这里到底发生了什么事?

测试任何东西都非常困难,因为它只是每个月发生一次,但我们很欣赏任何有关实际情况的理论。

感谢您的任何建议!

2 个答案:

答案 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}}的访问权限(非常非常重要)