C# 如何使用TPL并行化文件写入?
我试图将字符串列表保存到多个文件中,每个字符串保存在不同的文件中,并同时保存。我是这样做的:C# 如何使用TPL并行化文件写入?,c#,.net,multithreading,task-parallel-library,C#,.net,Multithreading,Task Parallel Library,我试图将字符串列表保存到多个文件中,每个字符串保存在不同的文件中,并同时保存。我是这样做的: public async Task SaveToFilesAsync(string path, List<string> list, CancellationToken ct) { int count = 0; foreach (var str in list) { string fullPath = path + @"\" + count.ToStr
public async Task SaveToFilesAsync(string path, List<string> list, CancellationToken ct)
{
int count = 0;
foreach (var str in list)
{
string fullPath = path + @"\" + count.ToString() + "_element.txt";
using (var sw = File.CreateText(fullPath))
{
await sw.WriteLineAsync(str);
}
count++;
NLog.Trace("Saved in thread: {0} to {1}",
Environment.CurrentManagedThreadId,
fullPath);
if (ct.IsCancellationRequested)
ct.ThrowIfCancellationRequested();
}
}
try
{
var savingToFilesTask = SaveToFilesAsync(@"D:\Test", myListOfString, ct);
}
catch(OperationCanceledException)
{
NLog.Info("Operation has been cancelled by user.");
}
但在日志文件中,我可以清楚地看到保存总是发生在同一个线程id中,所以没有并行性?我做错了什么?如何修复它?我的目标是使用所有的计算机内核尽可能快地保存所有数据 如果这是在并行存储(SSD)上,您可以通过并行化来加快速度。由于没有内置的方法以一定的并行度并行异步循环,我建议PLINQ使用固定的并行度和同步IO<代码>并行。ForEach不能设置为具有固定的DOP(仅限最大DOP)。如果要进行并行,必须通知.NET进行。 我认为,如果您将代码拆分为一个附加函数,那么实现这一点的最简单方法之一就会变得清晰
public async Task WriteToFile(
string path,
string str,
int count)
{
var fullPath = string.Format("{0}\\{1}_element.txt", path, count);
using (var sw = File.CreateText(fullPath))
{
await sw.WriteLineAsync(str);
}
NLog.Trace("Saved in TaskID: {0} to \"{1}\"",
Task.CurrentId,
fullPath);
}
其思想是将实际的单个IO操作拆分为一个额外的异步函数,并在不等待它们的情况下调用这些函数,而是将它们存储在一个列表中,并在最后等待所有这些任务
我通常不会写C#代码,所以请原谅我可能会犯的任何语法错误:
public async Task SaveToFilesAsync(string path, List<string> list, CancellationToken ct)
{
int count = 0;
var writeOperations = new List<Task>(list.Count);
foreach (var str in list)
{
string fullPath = path + @"\" + count.ToString() + "_element.txt";
writeOperations.add(SaveToFileAsync(fullPath, str, ct));
count++;
ct.ThrowIfCancellationRequested();
}
await Task.WhenAll(writeOperations);
}
private async Task SaveToFileAsync(string path, string line, CancellationToken ct)
{
using (var sw = File.CreateText(path))
{
await sw.WriteLineAsync(line);
}
NLog.Trace("Saved in thread: {0} to {1}",
Environment.CurrentManagedThreadId,
fullPath);
ct.ThrowIfCancellationRequested();
}
public异步任务SaveToFilesAsync(字符串路径、列表、取消令牌ct)
{
整数计数=0;
var writeOperations=新列表(List.Count);
foreach(列表中的var str)
{
字符串fullPath=path++“\”+count.ToString()+“\u element.txt”;
add(SaveToFileAsync(fullPath、str、ct));
计数++;
ct.ThrowIfCancellationRequested();
}
等待任务。WhenAll(写入操作);
}
专用异步任务SaveToFileAsync(字符串路径、字符串行、CancellationToken ct)
{
使用(var sw=File.CreateText(路径))
{
等待sw.WriteLineAsync(行);
}
NLog.Trace(“保存在线程:{0}到{1}中”,
Environment.CurrentManagedThreadId,
全程);
ct.ThrowIfCancellationRequested();
}
这样,IO操作由同一线程一次又一次地触发。这应该很快就能奏效。一旦使用.NET线程池完成IO操作,就会触发延续
我还删除了if(ct.IsCancellationRequested)
检查,因为这是由ct.throwifccancellationrequested()完成的代码>无论如何
希望这能给你一个解决这些问题的方法。本质上,你的问题是foreach
是同步的。它使用同步的IEnumerable
为了解决这个问题,首先将循环体封装到异步函数中
public async Task WriteToFile(
string path,
string str,
int count)
{
var fullPath = string.Format("{0}\\{1}_element.txt", path, count);
using (var sw = File.CreateText(fullPath))
{
await sw.WriteLineAsync(str);
}
NLog.Trace("Saved in TaskID: {0} to \"{1}\"",
Task.CurrentId,
fullPath);
}
然后,不要同步循环,而是将字符串序列投影到执行封装循环体的任务序列中。这本身不是一个异步操作,但投影不会阻塞,即没有wait
然后等待他们按照任务调度器定义的顺序完成所有任务
public async Task SaveToFilesAsync(
string path,
IEnumerable<string> list,
CancellationToken ct)
{
await Task.WhenAll(list.Select((str, count) => WriteToFile(path, str, count));
}
public异步任务SaveToFilesAsync(
字符串路径,
IEnumerable列表,
取消令牌(ct)
{
wait Task.WhenAll(list.Select((str,count)=>WriteToFile(path,str,count));
}
没有要取消的内容,因此没有必要向下传递取消令牌
我使用了Select
的索引重载来提供count
值
我已将您的日志代码更改为使用当前任务ID,这避免了计划方面的任何混乱。我已在原始问题中添加了我的答案,是否应将其添加到此处?
编辑:下面是建议的解决方案,它现在并行运行多个save
您需要将foreach循环(从第一项到最后一项依次运行)替换为Parallel.foreach()循环,该循环可以配置为并行
var cts = new CancellationTokenSource();
Task.WaitAll(SaveFilesAsync(@"C:\Some\Path", files, cts.Token));
cts.Dispose();
然后在该方法中进行并行处理
public async Task SaveFilesAsync(string path, List<string> list, CancellationToken token)
{
int counter = 0;
var options = new ParallelOptions
{
CancellationToken = token,
MaxDegreeOfParallelism = Environment.ProcessorCount,
TaskScheduler = TaskScheduler.Default
};
await Task.Run(
() =>
{
try
{
Parallel.ForEach(
list,
options,
(item, state) =>
{
// if cancellation is requested, this will throw an OperationCanceledException caught outside the Parallel loop
options.CancellationToken.ThrowIfCancellationRequested();
// safely increment and get your next file number
int index = Interlocked.Increment(ref counter);
string fullPath = string.Format(@"{0}\{1}_element.txt", path, index);
using (var sw = File.CreateText(fullPath))
{
sw.WriteLine(item);
}
Debug.Print(
"Saved in thread: {0} to {1}",
Thread.CurrentThread.ManagedThreadId,
fullPath);
});
}
catch (OperationCanceledException)
{
Debug.Print("Operation Canceled");
}
});
}
public async Task savefilesaync(字符串路径、列表、取消令牌)
{
int计数器=0;
var options=新的并行选项
{
CancellationToken=令牌,
MaxDegreeOfParallelism=Environment.ProcessorCount,
TaskScheduler=TaskScheduler.Default
};
等待任务。运行(
() =>
{
尝试
{
并行ForEach(
列表
选项,
(项目、状态)=>
{
//如果请求取消,这将抛出在并行循环外部捕获的OperationCanceledException
options.CancellationToken.ThrowIfCancellationRequested();
//安全地增加并获取下一个文件号
int index=联锁增量(参考计数器);
string fullPath=string.Format(@“{0}\{1}{u element.txt”,路径,索引);
使用(var sw=File.CreateText(完整路径))
{
软件写入线(项目);
}
调试。打印(
“保存在以下线程中:{0}到{1}”,
Thread.CurrentThread.ManagedThreadId,
全程);
});
}
捕获(操作取消异常)
{
调试。打印(“操作已取消”);
}
});
}
您的操作是异步运行的,但不是并行运行,这是有区别的。您可以将循环并行化,但我认为这不会产生任何改进-您的操作不受CPU限制,而是受IO限制…您不太可能