DataTable的线程安全性

时间:2014-01-23 14:20:40

标签: c# multithreading datatable ado.net

我已经阅读了这个答案ADO.NET DataTable/DataRow Thread Safety,并且无法理解某些事情。 特别是我不能理解[2]文章。我需要使用什么样的包装? 谁能举个例子?

此外,我无法理解作者的意思是谈论级联锁和完全锁定。请举例。

3 个答案:

答案 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不会将内容放入列表或类似内容中)
  • 因此,行可以立即开始流 ,而无需等待整个文件首先进行预处理
  • 没有内存饱和问题
  • 无线程并发症/间接费用
  • 你要保留原始订单(通常不是关键,但很好)
  • 您只受到读取原始文件的速度的限制,原始文件在单个线程上通常更快,而不是来自多个线程(单个IO设备上的争用只是开销)< / LI>
  • 避免了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。