Parallel.ForEach выдает исключение при извлечении zip-файла

Я читаю содержимое zip-файла и пытаюсь его извлечь.

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

Теперь, если я извлекаю цикл 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.InvalidDataException» возникло в System.IO.Compression.dll, но не было обработано в пользовательском коде.

Дополнительная информация: Заголовок локального файла поврежден.

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

        });

И чтобы избежать этого, я мог бы использовать блокировку, но это противоречит всей цели.

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

        });

Любой другой или лучший способ извлечь файлы?


person Simsons    schedule 23.05.2017    source источник
comment
Проблема в том, что вы прочитали один zip-файл и пытаетесь распаковать его параллельно. Что вы можете сделать, так это прочитать его в память, а затем извлечь параллельно, однако в конце строки, когда Windows попадает на диск, ваш ввод-вывод записи все равно будет непараллельным.   -  person rolls    schedule 23.05.2017


Ответы (2)


ZipFile явно задокументирован как негарантированный потокобезопасный для членов экземпляра. Это больше не упоминается на странице. Снимок за ноябрь 2016 г..

То, что вы пытаетесь сделать, невозможно сделать с этой библиотекой. могут существовать другие библиотеки, которые поддерживают несколько потоков на один zip-файл, но я бы этого не ожидал.

Вы можете использовать многопоточность для одновременного распаковывания нескольких файлов, но не для нескольких записей в одном ZIP-файле.

person Rob    schedule 23.05.2017
comment
Затем вам понадобится несколько экземпляров ZipFile. Все должно быть в порядке, так как он просто читает почтовый индекс. - person Stuart Axon; 10.12.2018
comment
Хотя я согласен с тем, что ZipFile не является потокобезопасным, связанная страница не содержит ссылок на потокобезопасность. - person Mitch Wheat; 27.01.2019
comment
@Stuart Axon: с этим все должно быть в порядке, так как он просто читает zip .. - это не мой недавний опыт; даже параллельное чтение из ZipArchive вызывает ошибки. - person Mitch Wheat; 27.01.2019
comment
@MitchWheat Ха, похоже, что страница была обновлена. Это определенно раньше говорило так: v=vs.110).aspx" rel="nofollow noreferrer">web.archive.org/web/20161111235120/https://msdn.microsoft.com/ - person Rob; 27.01.2019

Параллельная запись/чтение не является хорошей идеей, так как контроллер жесткого диска будет выполнять запросы только один за другим. Имея несколько потоков, вы просто добавляете накладные расходы и ставите их в очередь без какой-либо выгоды.

Попробуйте сначала прочитать файл в память, это позволит избежать вашего исключения, однако, если вы проверите его, вы можете обнаружить, что он на самом деле медленнее из-за накладных расходов на большее количество потоков.

Если файл очень большой и распаковка занимает много времени, параллельное выполнение распаковки может улучшить скорость, однако чтение/запись ввода-вывода не улучшится. В любом случае, большинство библиотек декомпрессии уже являются многопоточными, поэтому, только если это не так, вы получите какой-либо прирост производительности от этого.

Редактировать: хитрый способ сделать библиотечный поток безопасным ниже. Это работает медленнее/на уровне в зависимости от 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();
person rolls    schedule 23.05.2017
comment
Это не ответ и хорошо подходит для комментариев - person Simsons; 23.05.2017
comment
Любой другой или лучший способ извлечь файлы? почти уверен, что это объясняет лучший способ сделать это и почему. - person rolls; 23.05.2017
comment
Как, Это объясняет? Я уже упомянул 3 рассматриваемых подхода, и ваш ответ (комментарий) относится к одному из них и не ясен ни один из подходов. Чем он отличается и лучше, чем эти 3 подхода? - person Simsons; 23.05.2017
comment
Ваш подход ошибочен и не будет работать по причинам, описанным выше. - person rolls; 23.05.2017
comment
Что значит ущербный? Вы читали вопрос? Есть 3 подхода, и я не понимаю, как 1-й и 3-й не сработают. Можете ли вы написать несколько строк, чтобы показать, что вы подразумеваете под другим. - person Simsons; 24.05.2017
comment
Они работают, но это противоречит цели использования parallel. Для каждого. Запустите тест для вариантов 1 и 3, и вы, вероятно, обнаружите, что 1 работает быстрее, даже если вы изменили вариант 3, чтобы он работал без блокировки (например, сначала читал в память). Весь смысл моего ответа в том, что попытка распараллелить эту проблему не дает выигрыша в производительности. Вы сделали предположение, что параллельно он будет работать быстрее, но это не так. - person rolls; 24.05.2017
comment
Нет никаких предположений, что вариант 3 будет работать быстрее, чем вариант 1. И это даже не вопрос. Вопрос в том, как/можно ли мы использовать вариант 2 без каких-либо ошибок и без использования блокировки, как в варианте 3, поскольку он побеждает всю цель. - person Simsons; 24.05.2017
comment
Если он не будет работать быстрее, то почему вы хотите использовать Parallel.ForEach? Это добавляет накладные расходы без какой-либо выгоды. Если вы хотите запустить его без блокировки, вам нужно сначала прочитать zip-файл в память, создать несколько его копий в памяти, а затем передать несколько экземпляров в цикл ForEach. Это будет медленнее, чем вариант 1, поэтому нет смысла, который я пытался объяснить. - person rolls; 24.05.2017
comment
Я добавил пример того, как вы можете заставить его работать. Однако я советую против этого, поскольку это почти наверняка будет медленнее и менее ремонтопригодно. - person rolls; 24.05.2017
comment
Что делает clonedZipEntries? Зачем тебе это вообще нужно. если вы не можете нарисовать или предоставить понятное решение, нет смысла предоставлять теории, поскольку они уже есть в msdn, и вот некоторые чтения stackoverflow.com/help/how-to-answer - person Simsons; 24.05.2017
comment
Потому что, поскольку библиотека не является потокобезопасной, вы должны сделать несколько ее клонов. Еще раз, то, что вы пытаетесь сделать, не сработает. Пожалуйста, прочитайте мой первоначальный ответ вместе с другим ответом о том, почему Parallel.ForEach не подходит для того, что вы пытаетесь сделать. - person rolls; 24.05.2017
comment
Я опубликовал рабочий пример, показывающий, как сделать его потокобезопасным. Бенчмарк показывает, что производительность медленнее/одинакова в зависимости от архива, например, это не подходит для Parallel.ForEach. - person rolls; 24.05.2017
comment
Я знаю, что TPL или параллели - плохая идея при операциях ввода-вывода или баз данных. Но это было ограничение с тех пор, как оно было введено, но я проверял, были ли какие-либо обновления, чтобы заставить его работать. Возможно, это не ограничение TPL, а самой ОС. - person Simsons; 25.05.2017
comment
Это не ограничение языка. Это связано с тем, как физически работает жесткий диск. Он может записывать только один сектор за раз, поэтому любые операции ввода-вывода будут стоять в очереди независимо от того, что вы делаете в программном обеспечении или ОС. - person rolls; 25.05.2017