我正在尝试将字符串列表保存到多个文件,每个字符串保存在不同的文件中,并同时执行。我是这样做的:
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中,所以没有并行性发生?我究竟做错了什么?怎么解决?我的目标是使用所有计算机核心尽快保存所有内容。
答案 0 :(得分:4)
基本上,您的问题是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);
}
然后,不是同步循环,而是将字符串序列投影到执行封装循环体的任务序列。这本身不是异步操作,但投影不会阻塞,即没有await
。
然后等待所有任务以任务计划程序定义的顺序完成。
public async Task SaveToFilesAsync(
string path,
IEnumerable<string> list,
CancellationToken ct)
{
await Task.WhenAll(list.Select((str, count) => WriteToFile(path, str, count));
}
没有什么可以取消,所以没有必要将取消令牌传递下去。
我使用了Select
的索引重载来提供count
值。
我已将您的日志记录代码更改为使用当前的任务ID,这可以避免任何有关日程安排的混淆。
答案 1 :(得分:2)
如果你想并行执行,你必须告诉.NET这样做。 我认为,如果将代码拆分为附加功能,最简单的方法之一就变得清晰了。
我们的想法是将实际的单个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();
}
这样,IO操作将由同一个线程一个接一个地触发。这应该非常快。一旦使用.NET ThreadPool完成IO操作,就会触发继续。
我还删除了if (ct.IsCancellationRequested)
支票,因为这是ct.ThrowIfCancellationRequested();
完成的。
希望能让你了解如何处理这些事情。
答案 2 :(得分:1)
如果这是在并行存储(SSD)上,您可以通过并行化来加快速度。由于没有内置的方法来并行化具有一定程度并行性的异步循环,因此我建议使用具有固定并行度和同步IO的PLINQ。无法使Parallel.ForEach
具有固定的DOP(仅限最大DOP)。
答案 3 :(得分:1)
我在原始问题中添加了我的答案,我应该在此处添加吗? C# TPL calling tasks in a parallel manner and asynchronously creating new files
修改:此处提出的解决方案现在可以并行运行多个保存。
你需要用一个可以配置为并行性的Parallel.ForEach()循环来替换从第一个项到最后一个项顺序运行的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");
}
});
}