关于c#:DataTable的线程安全

Thread safety for DataTable

我已经阅读了ADO.NET DataTable / DataRow线程安全性的此答案,并且无法理解某些内容。
特别是我听不懂[2]文章。 我需要使用哪种包装纸?
谁能举一个例子?

我也无法理解作者所说的级联锁和全锁。 也请举例。


DataTable根本不是为同时使用而设计或设计的(尤其是在涉及任何形式的突变的情况下)。我认为,此处建议的"包装器"可以是:

  • 无需同时处理DataTable(涉及突变时),或者:
  • 删除DataTable,而不是使用直接支持您需要的数据结构(例如并发集合),或者更简单并且可以轻松同步的数据结构(互斥或读取器/写入器)

基本上:改变问题。

来自评论:

The code looks like:

1
2
3
4
5
6
7
8
9
10
11
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 ,因为待处理行已被共享。最好的选择是:

1
2
3
4
5
6
Parallel.ForEach(strings, str=> {
    object[] values = MyParser.Parse(str);
    lock(table) {
        table.Rows.Add(values);
    }
});

上面的重要更改是lock涵盖了整个新行过程。请注意,使用像这样的Parallel.ForEach时,您将无法保证顺序,因此重要的是最终顺序不必完全匹配(如果数据包含时间分量,这应该不成问题)。

然而!我仍然认为您采用了错误的方法:要使并行性有意义,它必须是非平凡的数据。如果您有非平凡的数据,那么您确实不需要将其全部缓存在内存中。我强烈建议您执行以下操作,在单个线程上可以正常工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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设备上的争用只是开销)
  • 避免了DataTable的所有开销,这在这里是过大的-因为它是如此灵活,所以开销很大
  • (从日志文件中)读取和(向数据库中)写入现在是并发的,而不是顺序的

我在自己的工作中做了很多类似^^^的事情,从经验来看,它通常至少比首先在内存中填充DataTable快两倍。

最后-这是一个IEnumerable< T >实现的示例,该实现接受并发的读取器和写入器,而无需将所有内容都缓存在内存中-这将允许多个线程通过一个线程解析数据(调用Add,最后是Close)通过IEnumerable< T > API用于SqlBulkCopy的线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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();


    }

}


面对相同的问题,我决定实现嵌套的ConcurrentDictionaries

它是通用的,但可以更改为使用定义的类型。
包含转换为DataTable的示例方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/// <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;
    }
}

介绍

如果并发或并行性是DataTable对象程序的要求,则可以做到这一点。我们来看两个示例(基本上,我们将在所有示例中看到常见的AsEnumerable()方法的使用):

在DataTable上进行1-并行迭代:

.NET提供了本机资源以在DataTable上并行进行迭代,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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的核心不是线程安全的集合/矩阵。然后,您需要ConcurrentBag的支持以保证不破坏您代码上的Exception。

在" ConcurrentBag-是否添加多个项目?"中,考虑到编程需要在DataTables上使用并发性,我编写了一个详细的示例,其中将DataTable对象中的项目添加到ConcurrentBag派生类中。然后,可以在使用并行资源的ConcurrentBag上添加了程序业务规则之后,将ConcurrentBag集合转换为DataTable。