我一直在重写一些流程密集型循环来使用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行,可以获得任何收益吗?
答案 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
}
Parallel.ForEach
为您提供一个。不同之处在于它不是
反击但指数。如果我改变了预期的行为,你可以
仍然添加计数器并使用Interlocked.Increment
增加它。streamwriter.AutoFlush = true
,这会影响性能,您可以将其设置为false
并在完成所有数据写入后将其刷新。如果可能的话,将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);
});