C#Parallel.foreach - 使变量线程安全

时间:2014-11-17 14:50:45

标签: c# locking task-parallel-library parallel.foreach

我一直在重写一些流程密集型循环来使用TPL来提高速度。这是我第一次尝试线程,所以想要检查我正在做的是正确的方法。

结果很好 - 当从标准DataTable循环移动到foreach循环时,处理Parallel.ForEach中1000行的数据将处理时间从34分钟减少到9分钟。对于此测试,我删除了非线程安全操作,例如将数据写入日志文件并递增计数器。

我仍然需要写回日志文件并增加一个计数器,所以我尝试实现一个包含streamwriter / increment代码块的锁。

FileStream filestream = new FileStream("path_to_file.txt", FileMode.Create);
StreamWriter streamwriter = new StreamWriter(filestream);
streamwriter.AutoFlush = true;

try
{
    object locker = new object();

    // Lets assume we have a DataTable containing 1000 rows of data.
    DataTable datatable_results;

    if (datatable_results.Rows.Count > 0)
    {
        int row_counter = 0;

        Parallel.ForEach(datatable_results.AsEnumerable(), data_row =>
        { 
            // Process data_row as normal.

            // When ready to write to log, do so.
            lock (locker)
            {
                row_counter++;
                streamwriter.WriteLine("Processing row: {0}", row_counter);

                // Write any data we want to log.
            }
        });                    
    }
}
catch (Exception e)
{
    // Catch the exception.
}
streamwriter.Close();

以上似乎按预期工作,性能成本最低(仍然是9分钟的执行时间)。当然,锁中包含的操作本身并不重要 - 我假设随着锁中处理代码所花费的时间增加,线程锁定的时间越长,它对处理时间的影响就越大。

我的问题:以上是一种有效的方法吗?或者是否有一种不同的方式来实现上述更快或更安全?

另外,假设我们原来的DataTable实际上包含30000行。通过将此DataTable拆分为每行1000行,然后在Parallel.ForEach中处理它们而不是一次处理所有300000行,可以获得任何收益吗?

5 个答案:

答案 0 :(得分:5)

写入文件很昂贵,在写入文件时你持有一个独占锁,这很糟糕。它会引入争论。

您可以将其添加到缓冲区中,然后一次性写入文件。这应该消除争用并提供扩展方式。

if (datatable_results.Rows.Count > 0)
{
    ConcurrentQueue<string> buffer = new ConcurrentQueue<string>();
    Parallel.ForEach(datatable_results.AsEnumerable(), (data_row, state, index) =>
    {
        // Process data_row as normal.

        // When ready to write to log, do so.

        buffer.Enqueue(string.Format( "Processing row: {0}", index));
    });

    streamwriter.AutoFlush = false;
    string line;
    while (buffer.TryDequeue(out line))
    {
        streamwriter.WriteLine(line);
    }
    streamwriter.Flush();//Flush once when needed
}
  1. 请注意,您不需要维护循环计数器, Parallel.ForEach为您提供一个。不同之处在于它不是 反击但指数。如果我改变了预期的行为,你可以 仍然添加计数器并使用Interlocked.Increment 增加它。
  2. 我发现您正在使用streamwriter.AutoFlush = true,这会影响性能,您可以将其设置为false并在完成所有数据写入后将其刷新。
  3. 如果可能的话,将StreamWriter包装在using语句中,这样你甚至不需要刷新流(你可以免费获得它)。

    或者,您可以查看完成其工作的日志框架。示例:NLog,Log4net等

答案 1 :(得分:1)

可能试图改进这一点,如果你避免记录,或只登录特定于线程的日志文件(不确定这是否对你有意义)

TPL启动尽可能多的线程Does Parallel.ForEach limits the number of active threads?

所以你可以做的是:

1)获取目标机器上的核心数量

2)创建一个计数器列表,其中包含尽可能多的元素

3)每个核心的更新计数器

4)在并行执行终止后将所有它们相加。

所以,在实践中:

//KEY(THREAD ID, VALUE: THREAD LOCAL COUNTER)
Dictionary<int,int> counters = new Dictionary<int, int>(NUMBER_OF_CORES); 
....
 Parallel.ForEach(datatable_results.AsEnumerable(), data_row =>
        { 
            // Process data_row as normal.

            // When ready to write to log, do so.
            //lock (locker) //NO NEED FOR LOCK, EVERY THREAD UPDATES ITS _OWN_ COUNTER
            //{
                //row_counter++;

                 counters[Thread.CurrentThread.ManagedThreadId].Value +=1;

                //NO WRITING< OR WRITING THREAD SPECIFIC FILE ONLY
                //streamwriter.WriteLine("Processing row: {0}", row_counter);


            //}
        });             
....

//AFTER EXECUTION OF PARALLEL LOOP SUM ALL COUNTERS AND GET TOTAL OF ALL THREADS. 

这样做的好处是根本没有锁定戏剧性地提高性能。当你使用.net concurent集合时,它们总是使用某种锁定。

这自然是一个基本想法,如果您复制粘贴,可能无法正常工作。我们谈论的是多线程,这一直是一个难题。但是,希望它能为你提供一些继续发表的想法。

答案 2 :(得分:1)

首先,在表中处理一行需要大约2秒钟,并且可能需要几毫秒来递增计数器并写入日志文件。由于实际处理比您需要序列化的部分多1000倍,因此该方法并不重要。

此外,您实施它的方式非常扎实。有一些方法可以优化它,但没有一种方法值得在您的情况下实施。

避免锁定增量的一种有用方法是使用Interlocked.Increment。它比x++慢一点,但比lock {x++;}快得多。但在你的情况下,这并不重要。

至于文件输出,请记住输出无论如何都要序列化,所以最多可以最大限度地减少锁中花费的时间。您可以通过在进入锁之前缓冲所有输出来执行此操作,然后只需在锁内执行写操作。您可能希望执行异步写入以避免对I / O进行不必要的阻塞。

答案 3 :(得分:0)

这是我使用并行的代码。这个概念类似,也许您可​​以更容易实现。仅供参考,为了进行调试,我在代码中保持定期for循环并有条件地编译并行代码。希望这可以帮助。但是,此方案中的i值与处理的记录数不同。您可以创建一个计数器并使用锁并为其添加值。对于我的其他代码,我有一个计数器,我没有使用锁,只是允许值可能关闭,以避免代码较慢。我有一个状态机制来指示处理的记录数。对于我的实现,计数不是问题的可能性很小 - 在循环结束时,我发出一条消息,说明所有记录都已处理完毕。

#if DEBUG
        for (int i = 0; i < stend.PBBIBuckets.Count; i++)
        {
            //int serverIndex = 0;
#else
        ParallelOptions options = new ParallelOptions();
        options.MaxDegreeOfParallelism = m_maxThreads;

        Parallel.For(0, stend.PBBIBuckets.Count, options, (i) =>

        {
#endif
            g1client.Message request;
            DataTable requestTable;

            request = new g1client.Message();

            requestTable = request.GetDataTable();

            requestTable.Columns.AddRange(
                Locations.Columns.Cast<DataColumn>().Select(x => new DataColumn(x.ColumnName, x.DataType)).ToArray
                    ());

            FillPBBIRequestTables(requestTable, request, stend.PBBIBuckets[i], stend.BucketLen[i], stend.Hierarchies);
#if DEBUG
        }
#else
        });
#endif

答案 4 :(得分:0)

您可以使用新方法传输并行代码。例如:

    // Class scope
    private string GetLogRecord(int rowCounter, DataRow row)
    {
        return string.Format("Processing row: {0}", rowCounter); // Write any data we want to log.
    }

    //....
    Parallel.ForEach(datatable_results.AsEnumerable(), data_row =>
    { 
        // Process data_row as normal.

        // When ready to write to log, do so.
        lock (locker)
            row_counter++;

        var logRecord = GetLogRecord(row_counter, data_row);

        lock (locker)
            streamwriter.WriteLine(logRecord);
    });