C# DataTable并发操作:索引超出了数组界限

场景

winform实现的OPC客户端,订阅数据更新时读取DataRow的数据不定时报出索引越界异常,debug模式下监控DataRow对象没有问题,对应索引列上有相应的值。现象非常离奇,异常不定时出现。订阅数据点量大,更新频率毫秒级,调试无法定位到具体原因。

排查异常原因

很容易就确认是多线程并发读写DataTable有问题,因此首先从DataTable的用法上查找原因。

  • DataTable多线程下使用
    程序中多线程处理的地方已经加锁处理,开始以为时锁使用不当,查阅了大量相关资料以及回顾以往的使用经历发现DataTable的应用没有问题。贴出部分代码。
lock (dt.Columns.SyncRoot)
{
   lock (dt.Rows.SyncRoot)
    {
        DataRow drTarget = dt.NewRow();
        DataRow dr = null;
        try
        {
            for (int i = 0; i < NumItems; i++)
            {
                for (int j = 0; j < itemDic.Count; j++)
                {
                    if (Convert.ToInt32(ClientHandles.GetValue(i + 1)) == Convert.ToInt32(itemDic.Keys.ToArray()[j]))
                    {
                        if (ItemValues.GetValue(i + 1) != null)
                        {
                            var clientHandle = ClientHandles.GetValue(i + 1);
                            dr = dt.AsEnumerable().First(drtemp => { return drtemp["clientHandle"].ToString() == clientHandle.ToString(); });
                            if (dr != null)
                            {
                                string drvalue_old = dr["itemValue"].ToString() == "" ? "0" : dr["itemValue"].ToString();
                                string drvalue = ItemValues.GetValue(i + 1).ToString();
                                DateTime drtimestamp = DateTime.Parse(TimeStamps.GetValue(i + 1).ToString());
                                setCellValue(dr, "itemValue", ItemValues.GetValue(i + 1));
                                setCellValue(dr, "itemQuality", Qualities.GetValue(i + 1));
                                setCellValue(dr, "itemTimeStamp", drtimestamp);
                                dataUpLoad(upstreamType, itemDic[clientHandle.ToString()], drvalue, drtimestamp.ToString());
                            }
                        }
                    }
                }
            }
        }
    }             
}                                
  • Code Review
    没有办法,看着日志文件中的异常信息难受,决定从头走查一遍代码。终于发现了端倪,在DataRow对象写操作时采用了异步的方式,看到这时一下觉醒,这里异步就会有问题。
void setCellValue(DataRow dr, string column, object value)
{
    try
    {
        if (value != null && value.Equals(""))
        {
            value = DBNull.Value;
        }
        if (this.InvokeRequired)
        {
            //this.BeginInvoke(new Action(() => { dr[column] = value; }));//1
            this.Invoke(new Action(() => { dr[column] = value; }));//2
        }
        else
            dr[column] = value;
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message + " | " + ex.StackTrace);
    }
}

修改代码用2的方式,测试异常不再出现。上面代码执行委托时1是异步赋值,2是同步赋值,数据处理线程中DataTabel的操作是在锁下进行的,但1的方式委托给另外的线程进行DataRow的异步写操作,这时数据处理线程进行DataRow对象的读操作就可能有问题,所以这里并发出现问题。
查看以前数据采集工具中的用法也是用的第2种形式,所以没碰到该问题。

  • 再次出现问题
    生产环境中运行一段时间程序UI发生hang,这是因为生产环境数据量大UI线程堵塞了,于是还是要使用异步赋值。更改赋值核心代码解决问题。
if (this.InvokeRequired)
{
    this.BeginInvoke(new Action(() => {
        lock (dr.Table.Columns.SyncRoot)
        {
            lock (dr.Table.Rows.SyncRoot)
            {
                dr[column] = value;
            }
        }
    }));
}

结论

Invoke和BeginInvoke是有很大区别的,使用锁的代码块内部进行委托操作时不要使用异步。如果必须使用异步并发操作对象就要加锁。开始我就是忽略了这一点,并不是DataTable的用法有问题。

欢迎来访

  • 有问题欢迎留言或加交流qq:825121848
  • 转载请注明出处
  • 请小编喝茶~
Last Updated: 4/16/2022, 11:05:56 AM