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

Wag*_*ior 0 c# multithreading locking serial-port volatile

我有一个类从一个串行读取数据,具有高阈值(1个字节).
我有一个变量存储来自串口的所有数据:_dataReceived.

private volatile string _dataReceived;
Run Code Online (Sandbox Code Playgroud)

我正在使用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);
    }
Run Code Online (Sandbox Code Playgroud)

处理它,包括从变量中删除它并处理答案.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;
        }
        ...
    }
Run Code Online (Sandbox Code Playgroud)

这有99.9%的时间.
但有时我会在这一行得到一个ArgumentOutOfRangeException:

                cmd = _dataReceived.Substring(0, idx);
Run Code Online (Sandbox Code Playgroud)

现在,我的问题是:我的变量是volatile,这意味着我总是访问真正的值而不是缓存.我很确定我的DataReceived事件一直在增加(快速),但是我使用lock语句来阻止任何其他线程更改此值.如果没有在字符串中使用NewLine,就无法运行这段代码(子字符串).而且这个IndexOf无法从字符串中返回索引.

那么......这里到底发生了什么?

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

感谢您的任何建议!

Han*_*ant 6

易失性的关键字是从不同步问题的解决方案.该错误清晰可见,您做了一个很难的假设,即在ParseAnswers()消耗字符串并完成运行之前,DataReceived事件处理程序不会再次执行.这是一厢情愿的想法,当事件处理程序再次触发时,您的代码崩溃并在ParseAnswers()解析时替换字符串.使用volatile实际上使你的代码容易崩溃:)你也应该注意到数据丢失,当ParseAnswers()运行得太晚时会发生这种情况.

解决方案非常简单,给ParseAnswers()一个参数.传递字符串.

使用Invoke()而不是BeginInvoke()也是一种解决方案,SerialPort确保DataReceived在执行时不能被触发.但它非常危险,当你试图调用Close()时容易使程序死锁.


Sri*_*vel 5

这就是我们建议在私有只读字段上使用锁的原因.

dataReceived_port_DataReceived执行ParseAnswers上一个事件时,您可以通过方法中的另一个线程更改(因为访问未同步).

所以会发生什么,现在两个线程竞争lock(_dataReceived),它们都被允许,因为它们都使用锁对象的不同引用.

记住lock语句适用于引用,_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);
        }
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

请注意,我已经删除了volatile修改(这是多余的),还我同步访问_dataReceived_port_DataReceived方法也(非常非常重要)