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
- 转载请注明出处
- 请小编喝茶~