【问题标题】:Parallel.ForEach throws exception when extracting a zip file提取 zip 文件时,Parallel.ForEach 抛出异常
【发布时间】:2017-05-23 01:20:03
【问题描述】:

我正在阅读一个 zip 文件的内容并尝试提取它们。

  var allZipEntries = ZipFile.Open(zipFileFullPath, ZipArchiveMode.Read).Entries;

现在,如果我提取 using Foreach 循环,则可以正常工作。缺点是它相当于 zip.extract 方法,当打算提取所有文件时我没有任何优势。

   foreach (var currentEntry in allZipEntries)
        {
            if (currentEntry.FullName.Equals(currentEntry.Name))
            {
                currentEntry.ExtractToFile($"{tempPath}\\{currentEntry.Name}");
            }
            else
            {
                var subDirectoryPath = Path.Combine(tempPath, Path.GetDirectoryName(currentEntry.FullName));
                Directory.CreateDirectory(subDirectoryPath);
                currentEntry.ExtractToFile($"{subDirectoryPath}\\{currentEntry.Name}");
            }

        }

现在要利用 TPL,尝试使用 Parallel.forEach,但这会引发以下异常:

System.IO.Compression.dll 中出现“System.IO.InvalidDataException”类型的异常,但未在用户代码中处理

附加信息:本地文件头已损坏。

  Parallel.ForEach(allZipEntries, currentEntry =>
        {
            if (currentEntry.FullName.Equals(currentEntry.Name))
            {
                currentEntry.ExtractToFile($"{tempPath}\\{currentEntry.Name}");
            }
            else
            {
                var subDirectoryPath = Path.Combine(tempPath, Path.GetDirectoryName(currentEntry.FullName));
                Directory.CreateDirectory(subDirectoryPath);
                currentEntry.ExtractToFile($"{subDirectoryPath}\\{currentEntry.Name}");
            }

        });

为了避免这种情况,我可以使用 lock ,但这违背了整个目的。

        Parallel.ForEach(allZipEntries, currentEntry =>
        {
            lock (thisLock)
            {
                if (currentEntry.FullName.Equals(currentEntry.Name))
                {
                    currentEntry.ExtractToFile($"{tempPath}\\{currentEntry.Name}");
                }
                else
                {
                    var subDirectoryPath = Path.Combine(tempPath, Path.GetDirectoryName(currentEntry.FullName));
                    Directory.CreateDirectory(subDirectoryPath);
                    currentEntry.ExtractToFile($"{subDirectoryPath}\\{currentEntry.Name}");
                }
            }

        });

还有其他或更好的方法来提取文件吗?

【问题讨论】:

  • 问题是您已经阅读了一个 zip 文件,并且您正在尝试并行提取它。您可以做的是将其读入内存,然后并行提取,但是在行的末尾,当 windows 碰到磁盘时,您的写入 IO 仍然是非并行的。

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


【解决方案1】:

ZipFile is explicitly documented as not guaranteed to be threadsafe for instance members。页面上不再提及这一点。 Snapshot from Nov 2016

你想要做的事情不能用这个库来完成。 可能还有一些其他库确实支持每个 zip 文件的多个线程,但我不希望它。

您可以使用多线程同时解压缩多个文件,但不能用于同一个 zip 文件中的多个条目。

【讨论】:

  • 您将需要多个 ZipFile 实例。应该没问题,因为它只是在读取 zip..
  • 虽然我同意 ZipFile 不是线程安全的,但链接页面不包含对线程安全的任何引用。
  • @Stuart Axon:“应该没问题,因为它只是在读取 zip..”——这不是我最近的经历;即使从 ZipArchive 并行读取也会引发错误。
  • @MitchWheat Huh,看起来页面已经更新了。肯定是这么说的:web.archive.org/web/20161111235120/https://msdn.microsoft.com/…
【解决方案2】:

并行写入/读取并不是一个好主意,因为硬盘控制器只会一个一个地运行请求。通过拥有多个线程,您只会增加开销并将它们全部排队而没有任何收益。

尝试先将文件读入内存,这将避免您的异常,但是如果您对它进行基准测试,您可能会发现它实际上更慢,因为更多线程的开销。

如果文件很大,解压时间比较长,并行解压可以提高速度,但是IO读写不会。无论如何,大多数解压库已经是多线程的,所以只有当这个库不是多线程时,你才能从这样做中获得任何性能提升。

编辑:使库线程在下面安全的一种狡猾的方法。这取决于 zip 存档的运行速度较慢/相当,这证明了这不会从并行性中受益

Array.ForEach(Directory.GetFiles(@"c:\temp\output\"), File.Delete);

Stopwatch timer = new Stopwatch();
timer.Start();
int numberOfThreads = 8;
var clonedZipEntries = new List<ReadOnlyCollection<ZipArchiveEntry>>();

for (int i = 0; i < numberOfThreads; i++)
{
    clonedZipEntries.Add(ZipFile.Open(@"c:\temp\temp.zip", ZipArchiveMode.Read).Entries);
}
int totalZipEntries = clonedZipEntries[0].Count;
int numberOfEntriesPerThread = totalZipEntries / numberOfThreads;

Func<object,int> action = (object thread) =>
{
    int threadNumber = (int)thread;
    int startIndex = numberOfEntriesPerThread * threadNumber;
    int endIndex = startIndex + numberOfEntriesPerThread;
    if (endIndex > totalZipEntries) endIndex = totalZipEntries;

    for (int i = startIndex; i < endIndex; i++)
    {
        Console.WriteLine($"Extracting {clonedZipEntries[threadNumber][i].Name} via thread {threadNumber}");
        clonedZipEntries[threadNumber][i].ExtractToFile($@"C:\temp\output\{clonedZipEntries[threadNumber][i].Name}");
    }

    //Check for any remainders due to non evenly divisible size
    if (threadNumber == numberOfThreads - 1 && endIndex < totalZipEntries)
    {
        for (int i = endIndex; i < totalZipEntries; i++)
        {
            Console.WriteLine($"Extracting {clonedZipEntries[threadNumber][i].Name} via thread {threadNumber}");
            clonedZipEntries[threadNumber][i].ExtractToFile($@"C:\temp\output\{clonedZipEntries[threadNumber][i].Name}");
        }
    }
    return 0;
};


//Construct the tasks
var tasks = new List<Task<int>>();
for (int threadNumber = 0; threadNumber < numberOfThreads; threadNumber++) tasks.Add(Task<int>.Factory.StartNew(action, threadNumber));

Task.WaitAll(tasks.ToArray());
timer.Stop();

var threaderTimer = timer.ElapsedMilliseconds;



Array.ForEach(Directory.GetFiles(@"c:\temp\output\"), File.Delete);

timer.Reset();
timer.Start();
var entries = ZipFile.Open(@"c:\temp\temp.zip", ZipArchiveMode.Read).Entries;
foreach (var entry in entries)
{
    Console.WriteLine($"Extracting {entry.Name} via thread 1");
    entry.ExtractToFile($@"C:\temp\output\{entry.Name}");
}
timer.Stop();

Console.WriteLine($"Threaded version took: {threaderTimer} ms");
Console.WriteLine($"Non-Threaded version took: {timer.ElapsedMilliseconds} ms");


Console.ReadLine();

【讨论】:

  • 这不是答案,非常适合 cmets
  • “还有其他或更好的方法来提取文件吗?”很确定这解释了一种更好的方法以及原因。
  • 如何,“这说明了”?我已经提到了 3 个有问题的方法,而您的回答(评论)指的是其中一个,并且不清楚任何方法。它与这 3 种方法有何不同和更好?
  • 您的方法有缺陷,由于上述原因无法正常工作。
  • 有缺陷是什么意思?你读过这个问题吗?有 3 种方法,我看不出第 1 和第 3 是如何工作的。你能写几行来说明你所说的不同是什么意思吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-24
  • 1970-01-01
  • 2012-06-14
  • 2013-10-27
  • 1970-01-01
相关资源
最近更新 更多