Классы ZipFile и ZipArchive из System.IO.Compression и асинхронного ввода-вывода

В.NET 4.5 добавлены новые классы для работы с zip-архивами. Теперь вы можете сделать что-то вроде этого:

using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
{
    foreach (ZipArchiveEntry entry in archive.Entries)
    {
        // Extract it to the file
        entry.ExtractToFile(entry.Name);

        // or do whatever you want
        using (Stream stream = entry.Open())
        {
            ...
        }
    }
}

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

Однако все операции ввода-вывода в этом коде будут выполняться в режиме блокировки, который считается "не крутым" в 2016 году. Итак, есть два вопроса:

  1. Можно ли получить асинхронный ввод / вывод с System.IO.Compression классы (или, может быть, с другой сторонней библиотекой.NET)?
  2. Есть ли смысл делать это? Я имею в виду, что алгоритмы сжатия / извлечения в любом случае очень сильно нагружают процессор, поэтому, если мы даже переключимся CPU-оценка блокируя ввод / вывод к асинхронному вводу / выводу, выигрыш в производительности может быть относительно небольшим (конечно, в процентах, а не в абсолютных значениях).

ОБНОВИТЬ:

Ответить на ответ Петра Дунихо: да, вы правы. По какой-то причине я не думал об этом варианте:

using (Stream zipStream = entry.Open())
using (FileStream fileStream = new FileStream(...))
{
    await zipStream.CopyToAsync(fileStream);
}

который определенно работает. Спасибо!

Кстати

await Task.Run(() => entry.ExtractToFile(entry.Name));

все еще будет быть связанной с CPU блокирующей операцией ввода-вывода, просто в отдельном потоке потреблять поток из пула потоков во время операций ввода-вывода.

Однако, как я вижу, разработчики.NET по-прежнему используют блокирующий ввод / вывод для некоторых операций архивирования (например, этот код для перечисления записей в архиве, например: ZipArchive.cs on dotnet @ github). Я также нашел открытую проблему об отсутствии асинхронного API для API ZipFile.

Я предполагаю, что в настоящее время у нас есть частичная асинхронная поддержка, но она далека от завершения.

2 ответа

Решение
  1. Можно ли получить асинхронный ввод / вывод с System.IO.Compression классы (или, может быть, с другой сторонней библиотекой.NET)?

В зависимости от того, что вы на самом деле подразумеваете под "асинхронным вводом / выводом", вы можете сделать это с помощью встроенных типов.NET. Например:

using (ZipArchive archive = await Task.Run(() => ZipFile.OpenRead(zipFilePath)))
{
    foreach (ZipArchiveEntry entry in archive.Entries)
    {
        // Extract it to the file
        await Task.Run(() => entry.ExtractToFile(entry.Name));

        // or do whatever you want
        using (Stream stream = entry.Open())
        {
            // use XXXAsync() methods on Stream object
            ...
        }
    }
}

Оберните это в XXXAsync() методы расширения, если хотите.

  1. Есть ли смысл делать это? Я имею в виду, что алгоритмы сжатия / извлечения в любом случае очень сильно нагружают ЦП, поэтому, если мы даже переключимся с ввода-вывода с привязкой к ЦП на асинхронный ввод-вывод, прирост производительности может быть относительно небольшим (конечно, в процентах, а не в абсолютных значениях).

Как минимум три причины сделать это:

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

Из обсуждения этого вопроса , который все еще открыт в Microsoft, используйте поток назначения сCopyToAsyncсделать настоящийasyncметод, а не просто фоновая задача.

      private static async Task ExtractToFileAsync(this ZipArchiveEntry source, string destinationFileName, bool overwrite, CancellationToken cancellationToken = default)
{
    const int bufferSize = 128 * 1024;

    if (source is null)
        throw new ArgumentNullException(nameof(source));

    if (destinationFileName is null)
        throw new ArgumentNullException(nameof(destinationFileName));

    var mode = overwrite ? FileMode.Create : FileMode.CreateNew;
    using (Stream destination = new FileStream(destinationFileName, mode, FileAccess.Write, FileShare.None, bufferSize, true))
    {
        using (var stream = source.Open())
        {
            await stream.CopyToAsync(destination, bufferSize, cancellationToken);
        }
    }

    File.SetLastWriteTime(destinationFileName, source.LastWriteTime.DateTime);
}

Другие вопросы по тегам