我已经阅读了这个答案ADO.NET DataTable/DataRow Thread Safety,并且无法理解某些事情。 特别是我不能理解[2]文章。我需要使用什么样的包装? 谁能举个例子?
此外,我无法理解作者的意思是谈论级联锁和完全锁定。请举例。
答案 0 :(得分:17)
DataTable
根本没有设计或打算用于并发使用(特别是在涉及任何形式的突变的情况下)。在我看来,这里可取的“包装”可能是:
DataTable
(涉及变异时),或:DataTable
,而不是使用直接支持您需要的数据结构(例如并发集合),或者更简单并且可以简单地同步(独占或读取/写入)的数据结构基本上:改变问题。
来自评论:
代码如下:
Parallel.ForEach(strings, str=> { DataRow row; lock(table){ row= table.NewRow(); } MyParser.Parse(str, out row); lock(table){ table.Rows.Add(row) } });
我只能希望out row
在这里是一个拼写错误,因为这实际上不会导致它填充通过NewRow()
创建的行,但是:如果你绝对必须使用这种方法,那么你无法使用NewRow
,因为待处理的行有点共享。你最好的选择是:
Parallel.ForEach(strings, str=> {
object[] values = MyParser.Parse(str);
lock(table) {
table.Rows.Add(values);
}
});
上述重要的变化是lock
涵盖了整个新的行过程。请注意,在使用这样的Parallel.ForEach
时,您无法保证订单,因此最终订单不需要完全匹配(如果数据包含时间组件,这不应该是一个问题)。
然而!我仍然认为你正在以错误的方式接近它:因为并行性是相关的,它必须是非平凡的数据。如果你有非平凡的数据,你真的不想在内存中缓冲它。我强烈建议执行类似以下的操作,这可以在单个线程上正常工作:
using(var bcp = new SqlBulkCopy())
using(var reader = ObjectReader.Create(ParseFile(path)))
{
bcp.DestinationTable = "MyLog";
bcp.WriteToServer(reader);
}
...
static IEnumerable<LogRow> ParseFile(string path)
{
using(var reader = File.OpenText(path))
{
string line;
while((line = reader.ReadLine()) != null)
{
yield return new LogRow {
// TODO: populate the row from line here
};
}
}
}
...
public sealed class LogRow {
/* define your schema here */
}
优点:
yield return
不会将内容放入列表或类似内容中)DataTable
的所有开销,这在这里是过度的 - 因为它非常灵活,但是有很大的开销我在自己的工作中做了很多像^^^这样的事情,从经验来看,它通常至少快两倍比首先在内存中填充DataTable
。
最后 - 这是一个IEnumerable<T>
实现的示例,它接受并发读取器和编写器而不需要在内存中缓冲所有内容 - 这将允许多个线程解析数据(调用Add
,最后Close
通过SqlBulkCopy
API使用IEnumerable<T>
的单个帖子{/ 1}}:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Acts as a container for concurrent read/write flushing (for example, parsing a
/// file while concurrently uploading the contents); supports any number of concurrent
/// writers and readers, but note that each item will only be returned once (and once
/// fetched, is discarded). It is necessary to Close() the bucket after adding the last
/// of the data, otherwise any iterators will never finish
/// </summary>
class ThreadSafeBucket<T> : IEnumerable<T>
{
private readonly Queue<T> queue = new Queue<T>();
public void Add(T value)
{
lock (queue)
{
if (closed) // no more data once closed
throw new InvalidOperationException("The bucket has been marked as closed");
queue.Enqueue(value);
if (queue.Count == 1)
{ // someone may be waiting for data
Monitor.PulseAll(queue);
}
}
}
public void Close()
{
lock (queue)
{
closed = true;
Monitor.PulseAll(queue);
}
}
private bool closed;
public IEnumerator<T> GetEnumerator()
{
while (true)
{
T value;
lock (queue)
{
if (queue.Count == 0)
{
// no data; should we expect any?
if (closed) yield break; // nothing more ever coming
// else wait to be woken, and redo from start
Monitor.Wait(queue);
continue;
}
value = queue.Dequeue();
}
// yield it **outside** of the lock
yield return value;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
static class Program
{
static void Main()
{
var bucket = new ThreadSafeBucket<int>();
int expectedTotal = 0;
ThreadPool.QueueUserWorkItem(delegate
{
int count = 0, sum = 0;
foreach(var item in bucket)
{
count++;
sum += item;
if ((count % 100) == 0)
Console.WriteLine("After {0}: {1}", count, sum);
}
Console.WriteLine("Total over {0}: {1}", count, sum);
});
Parallel.For(0, 5000,
new ParallelOptions { MaxDegreeOfParallelism = 3 },
i => {
bucket.Add(i);
Interlocked.Add(ref expectedTotal, i);
}
);
Console.WriteLine("all data added; closing bucket");
bucket.Close();
Thread.Sleep(100);
Console.WriteLine("expecting total: {0}",
Interlocked.CompareExchange(ref expectedTotal, 0, 0));
Console.ReadLine();
}
}
答案 1 :(得分:1)
面对同样的问题,我决定实现嵌套的ConcurrentDictionaries
它是通用的,但可以更改为使用定义的类型。 包含转换为DataTable的示例方法
/// <summary>
/// A thread safe data table
/// </summary>
/// <typeparam name="TX">The X axis type</typeparam>
/// <typeparam name="TY">The Y axis type</typeparam>
/// <typeparam name="TZ">The value type</typeparam>
public class HeatMap<TX,TY,TZ>
{
public ConcurrentDictionary<TX, ConcurrentDictionary<TY, TZ>> Table { get; set; } = new ConcurrentDictionary<TX, ConcurrentDictionary<TY, TZ>>();
public void SetValue(TX x, TY y, TZ val)
{
var row = Table.GetOrAdd(x, u => new ConcurrentDictionary<TY, TZ>());
row.AddOrUpdate(y, v => val,
(ty, v) => val);
}
public TZ GetValue(TX x, TY y)
{
var row = Table.GetOrAdd(x, u => new ConcurrentDictionary<TY, TZ>());
if (!row.TryGetValue(y, out TZ val))
return default;
return val;
}
public DataTable GetDataTable()
{
var dataTable = new DataTable();
dataTable.Columns.Add("");
var columnList = new List<string>();
foreach (var row in Table)
{
foreach (var valueKey in row.Value.Keys)
{
var columnName = valueKey.ToString();
if (!columnList.Contains(columnName))
columnList.Add(columnName);
}
}
foreach (var s in columnList)
dataTable.Columns.Add(s);
foreach (var row in Table)
{
var dataRow = dataTable.NewRow();
dataRow[0] = row.Key.ToString();
foreach (var column in row.Value)
{
dataRow[column.Key.ToString()] = column.Value;
}
dataTable.Rows.Add(dataRow);
}
return dataTable;
}
}
答案 2 :(得分:0)
简介
如果DataTable对象程序需要并发性或并行性,则可以做到这一点。让我们看两个示例(基本上,我们将看到在所有示例中普遍使用AsEnumerable()方法):
对DataTable进行1并行迭代:
.NET提供了本机资源以在DataTable上以并行方式进行迭代,如下所示:
DataTable dt = new DataTable();
dt.Columns.Add("ID");
dt.Columns.Add("NAME");
dt.Rows.Add(1, "One");
dt.Rows.Add(2, "Two");
dt.Rows.Add(3, "Three");
dt.PrimaryKey = new DataColumn[] { dt1.Columns["ID"] };
Parallel.ForEach(dt.AsEnumerable(), row =>
{
int rowId = int.Parse(row["ID"]);
string rowName = row["NAME"].ToString();
//TO DO the routine that useful for each DataRow object...
});
2-将多个项目添加到DataTable:
我认为这是不平凡的方法,因为DataTable的核心不是线程安全的集合/矩阵。那么,您需要ConcurrentBag的支持,以保证不破坏您代码中的Exception。
在“ ConcurrentBag - Add Multiple Items?”处,考虑到编程需要在DataTables上使用并发性,我编写了一个详细的示例,将DataTable对象中的项添加到ConcurrentBag派生类中。然后,可以在使用并行资源的ConcurrentBag上添加了程序业务规则之后,可以将ConcurrentBag集合转换为DataTable。