ADO.NET DataTable / DataRow线程安全

介绍

一位用户今天早上向我报告说,他遇到了一些问题,即我们作为内部框架的一部分提供的某些并行执行代码的结果不一致(即,列值有时会在它们不应该出现时为null)。 这段代码过去运行良好,最近没有被篡改过,但它让我想到了以下片段:

代码示例

lock (ResultTable) { newRow = ResultTable.NewRow(); } newRow["Key"] = currentKey; foreach (KeyValuePair output in outputs) { object resultValue = output.Value; newRow[output.Name] = resultValue != null ? resultValue : DBNull.Value; } lock (ResultTable) { ResultTable.Rows.Add(newRow); } 

(不保证编译,手工编辑以掩盖专有信息。)

说明

我们在系统中的其他位置都有这种级联类型的锁定代码,它工作正常,但这是我遇到的与ADO .NET交互的级联锁定代码的第一个实例。 众所周知,框架对象的成员通常不是线程安全的(在这种情况下就是这种情况),但是级联锁定应该确保我们不会同时读取和写入ResultTable.Rows。 我们很安全,对吗?

假设

好吧,级联锁定代码不能确保我们在向新行中的列分配的同时不读取或写入ResultTable.Rows。 如果ADO .NET使用某种缓冲区来分配非线程安全的列值,即使涉及不同的对象类型(DataTable与DataRow),该怎么办?

有没有人遇到过这样的事情? 我以为我会在StackOverflow问这里,然后在几个小时内打败我的脑袋:)

结论

嗯,共识似乎是将级联锁更改为完全锁已解决了这个问题。 这不是我预期的结果,但完全锁定版本在许多很多测试之后都没有产生问题。

教训:警惕在您无法控制的API上使用的级联锁。 谁知道封面下可能发生的事情!

阿伦,

我找不到您的方法的任何具体问题,而不是我的测试是详尽无遗的。 以下是我们坚持的一些想法(我们所有的应用程序都是以线程为中心的):

只要有可能:

[1]使所有数据访问完全primefaces化。 由于multithreading应用程序中的数据共享是各种无法预料的线程交互的绝佳场所。

[2]避免锁定类型。 如果类型不知道是线程安全的,请写一个包装器。

[3]包含允许快速识别正在访问共享资源的线程的结构。 如果系统性能允许,请将此信息记录在调试级别之上并低于通常的操作日志级别。

[4]任何代码,包括System。* et.al,内部未明确记录为线程安全测试不是线程安全。 传闻和其他人的口头表达并不重要。 测试并写下来。

希望这具有一定的价值。

我曾经读过一篇文章说他们发现内部在DataTable中使用公共行进行插入操作。 创建新记录的多个线程将覆盖公共行上的数据并相互腐蚀导致问题。 修复是在添加行时锁定表,因此一次只有一个线程可以添加新行。

您的代码对我来说也很好,但我建议您在添加新创建的行之前使用ResultTable.Rows.SyncRoot进行锁定,以便其他进程可以自由访问ResultTable对象的其余部分。

 lock (ResultTable.Rows.SyncRoot) 

.NET的这一点可能在过去七(!)年内有所改变,但为了回答这个问题,从.NET 4.7.1开始,列值缓冲的假设是不正确的。 通过查看corefx / DataRow.cs中的源代码 ,问题是_tempRecord字段周围的竞争条件,该字段存储行在数据表中的位置。 任何写入触发对BeginEditInternal()的调用都可能修改此字段,其中包括值更新。 当两个写入冲突时,最终会跟随另一个写入的_tempRecord值,因此会更新与预期不同的行。 这与Microsoft的文档一致,说明必须同步任何写入 (强调添加)。 Tony先前的回答描述了这种行为的一个子集。

作为一个例子,我最近通过上面的代码示例中的锁定方法破坏了代码,方法是改进性能。 代码是稳定的并且在没有问题的情况下运行了1。5年,但是,在每秒超过2000个新行的情况下,至少有几万个写入中的一个始终以错误的行结束。

一种可能的解决方法是锁定每次写入,但将它们分组以通过最小化锁定来限制性能影响。 另一种方法是给每个线程自己的表进行更新,然后合并结果。 在我的例子中,性能关键部分已经成为移动DataTable一段时间的候选者,因此使用更具可扩展性的数据结构进行了重新编码。