Добавлена распарельная распаковка.
Стандартный формат gzip трудно паралелится. Поэтому изменен для себя:
- В заголовке 6 бит 4 байта установлен в 1. Это зарезервированный бит и должен приводить к тому что другие архиваторы не испрользуют и при устновке определяют архив как невалидный.
- 5-8 байт содержат длину заархивированого блока с заголовком.
Для совместимости добавлена опция "compatibility". При установки создается gzip архив соответствующий стандарту. Для распаковки, определяется тип архива. Для архивов соответсвующих стандарту используется потоковый однопоточный режим как наиболее эффективный, но требуется .NET Core 3.0 или позднее.
Входной поток разбивается на куски каждый из которых сжимается отдельно (имеет заголовок, тело и CRC) и записывается в итоговый файл друг за другом. Итоговый формат полностью совместим со стандартом gzip и может быть распакован напрмер 7Zip.
Организовывается pipeline:
workFlow
.Step(new FileReadWorker(inFileStream, chunkSizeBytes))
.Pipe<BlockingPipe<FileChunk>>(size)
.Step<CompressWorker>(size)
.Pipe<OrderingPipe>(size)
.Step(new GzipWriteFileWorker(outFileStream))
Каждый Worker запускается в отдельном потоке. Потоки для сжатия запускается по количеству ядер процессора.
Для сихронизации потоков используются конкурентные коллекции (аля BlockingCollection) которые используются как Pip. Pip-ы ограничены по размеру, чтобы избежать излишнего потребления памяти при их забивании.
Для управления написана обертка WorkFlow (для красивого синтаксиса содержит внутрений класс). Для отмены используется стандартный механизм CancellationTokenSource.
Worker получились простые: pip на вход и pip на выход, что сильно упрощает их тестирование и поддержки.
При выбрасывание Exception из Worker (дочернего потока). Exception упаковывается в стандартный AggregateException. Worker-ы получают сигнал об отмене через CancellationTokenSource и могут завершить свою работу. Для ожидающий потоков вызывается Interrupt. (Так дочерние потоки не имеют дополнительной логики на отмену и просто должны прекратить работу, можно было бы использовать Thread.Abort(), но он был удален из .Net Core)
Ошибки работы с файлома читаемы и нетребует обертки или пояснения, выбрасываются как есть.
Однопоточный потому как GZipStream поддерживает multiple parts. Потребление CPU, памяти минимальное.
Время разархивации в разы меньше. Можно было бы искать в потоке заголовки (изветсная последовательность) и разбивать на куски но
- Могут быть ложные срабатывания и их нужно обрабатывать
- Из-за малой времени и потребляемых ресурсов. Накладные расходы на распареливание могут ухудшить резельтат.
- Внутрению колекция для OrderingPipe заменить на циклический буфер. Сейчас это по факту бутылочное горлышко.
- Тесты для Worker
- Обработка закрытия программы (Ctrl-C обрабатывается)
- Обработка нехватки памяти. Мы можем посчить сколько сумарно памяти нужно для запуска N потоков и если это больше доступной памяти запускаться в однопоточном режиме. При нехватки памяти Windows задействует swap что может быть хуже чем в однопоточном но в RAM.
Как альтернативное решение не использовать выделенные потоки а использовать самописный пул-потоков. При появление сообщения в Pip менеджер берет свободный поток и запускате на нем обработку только этого сообщения. Как это былобы реальзовано на Task.
Но нужны планировшики для кажго шага или один общий. И была идея переиспользовать стримы но GzipStream не поддерживает сброс. Поэтому большую часть аллокаций памяти сейчас от стримов. Для буферов используется шареная память.
Код вынесен в отдельную сборку. Минус в том что еще одна dll. Но сам проект .net standart 2.0 и может быть использована как в .Net таки и в .Net Core. Да, в рамках текущей задачи избыточно.
Скриншоты потребления памяти в /doc/img