Классы 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 году. Итак, есть два вопроса:
- Можно ли получить асинхронный ввод / вывод с
System.IO.Compression
классы (или, может быть, с другой сторонней библиотекой.NET)? - Есть ли смысл делать это? Я имею в виду, что алгоритмы сжатия / извлечения в любом случае очень сильно нагружают процессор, поэтому, если мы даже переключимся
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 ответа
- Можно ли получить асинхронный ввод / вывод с
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()
методы расширения, если хотите.
- Есть ли смысл делать это? Я имею в виду, что алгоритмы сжатия / извлечения в любом случае очень сильно нагружают ЦП, поэтому, если мы даже переключимся с ввода-вывода с привязкой к ЦП на асинхронный ввод-вывод, прирост производительности может быть относительно небольшим (конечно, в процентах, а не в абсолютных значениях).
Как минимум три причины сделать это:
- Процессоры очень быстрые. Во многих случаях ввод-вывод по-прежнему является узким местом, поэтому асинхронное ожидание ввода-вывода полезно.
- Многоядерные процессоры являются нормой. Поэтому полезно, чтобы одно ядро работало на декомпрессию, а другое выполняло другую работу.
- Асинхронные операции не полностью, а в некоторых случаях совсем не касаются производительности. Асинхронная обработка ваших архивов позволяет пользовательскому интерфейсу оставаться отзывчивым, что полезно.
Из обсуждения этого вопроса , который все еще открыт в 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);
}