如何使用TPL并行化文件写入?

时间:2015-06-24 07:49:16

标签: 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.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中,所以没有并行性发生?我究竟做错了什么?怎么解决?我的目标是使用所有计算机核心尽快保存所有内容。

4 个答案:

答案 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");
                }
            });
}