Безопасно ли параллельно вызывать ICsharpCode.SharpZipLib в нескольких потоках? - PullRequest
4 голосов
/ 05 мая 2011

В настоящее время мы используем для сжатия класс GZipOutputStream библиотеки ICsharpCode.SharpZipLib.Мы делаем это из одного потока.

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

Ответы [ 3 ]

11 голосов
/ 06 мая 2011

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

В библиотеке DotNetZip есть класс ParallelDeflateOutputStream, который делает то, что вы описываете.Класс задокументирован здесь .

Он может использоваться только для сжатия - без декомпрессии.Также это строго выходной поток - вы не можете read сжать.Учитывая эти ограничения, это в основном DeflateOutputStream, который внутренне использует несколько потоков.

Как это работает: он разбивает входящий поток на куски, а затем сбрасывает каждый кусок в отдельный рабочий поток для сжатия по отдельности.Затем он объединяет все эти сжатые потоки обратно в один упорядоченный поток в конце.

Предположим, что размер "порции", поддерживаемый потоком, составляет N байтов.Когда вызывающая сторона вызывает Write (), данные помещаются в буфер или порцию.Внутри метода Stream.Write(), когда первый «сегмент» заполнен, он вызывает ThreadPool.QueueUserWorkItem, выделяя сегмент для рабочего элемента.Последующие записи в поток начинают заполнять следующий сегмент, и когда , что заполнено, Stream.Write() снова вызывает QUWI.Каждый рабочий поток сжимает свой сегмент, используя «тип сброса» Sync (см. Спецификацию deflate), а затем отмечает свой сжатый большой двоичный объект, готовый к выводу.Эти различные выходные данные затем переупорядочиваются (поскольку фрагмент n не обязательно сжимается перед фрагментом n + 1) и записываются в поток выходных данных.Когда написано каждое ведро, оно помечается пустым и готово для повторного заполнения следующим Stream.Write().Каждый блок должен быть сжат с помощью типа синхронизации «Синхронизация», чтобы разрешить их повторную комбинацию посредством простой конкатенации, чтобы объединенный поток данных был легальным потоком DEFLATE.Конечный кусок должен Тип сброса = Готово.

Конструкция этого потока означает, что вызывающим не нужно писать с несколькими потоками. Вызывающие просто создают поток как обычно, как ванильный DeflateStream, используемый для вывода, и записывают в него,Потоковый объект использует несколько потоков, но ваш код не взаимодействует напрямую с ними.Код для «пользователя» ParallelDeflateOutputStream выглядит следующим образом:

using (FileStream raw = new FileStream(CompressedFile, FileMode.Create))
{
    using (FileStream input = File.OpenRead(FileToCompress))
    {
        using (var compressor = new Ionic.Zlib.ParallelDeflateOutputStream(raw))
        {
            // could tweak params of parallel deflater here
            int n;
            var buffer = new byte[8192];
            while ((n = input.Read(buffer, 0, buffer.Length)) != 0)
            {
                compressor.Write(buffer, 0, n);
            }                    
        }
    }
}

Он был разработан для использования в классе DotNetZip ZipFile, но он вполне пригоден для использования в качестве автономного сжатия выходного потока.Результирующий поток может быть ДЕДЕЛЬФАТИРОВАН (раздуваться?) Любым инфлятором.Результат полностью соответствует спецификации.

Поток настраивается.Вы можете установить размер используемых им буферов и уровень параллелизма.Он не создает сегменты без ограничений, потому что для больших потоков (в гигабайтах и ​​т. Д.) Это может привести к нехватке памяти.Таким образом, существует фиксированное ограничение на количество сегментов и, следовательно, степень параллелизма, которая может поддерживаться.

На моей двухъядерной машине этот класс потока почти удвоил скорость сжатия больших (100 МБ и более) файлов по сравнению со стандартным DeflateStream.У меня нет больших многоядерных машин, поэтому я не смог протестировать их дальше.Компромисс состоит в том, что параллельная реализация использует больше ЦП и больше памяти, а также сжимает немного менее эффективно (на 1% меньше для больших файлов) из-за синхронизации кадров, которую я описал выше.Преимущество в производительности будет зависеть от пропускной способности ввода-вывода в потоке вывода и от того, сможет ли хранилище идти в ногу с потоками параллельного компрессора.


Предупреждение:
Это поток DEFLATE, а не GZIP.Для отличий прочитайте RFC 1951 (DEFLATE) и RFC 1952 (GZIP) .

Но если вам действительно нужен gzip, источник этого потока доступен, так что вы можете просмотреть его и, возможно, получить некоторые идеи для себя.GZIP - это просто оболочка над DEFLATE с некоторыми дополнительными метаданными (такими как контрольная сумма Адлера и т. Д. - см. Спецификацию).Мне кажется, что построить ParallelGzipOutputStream будет не очень сложно, но, возможно, это тоже не тривиально.

Самой сложной задачей для меня было заставить семантику Flush () и Close () работать должным образом.


EDIT

Ради интереса я создал ParallelGZipOutputStream, который в основном выполняет то, что я описал выше, для GZip.Он использует задачи .NET 4.0 вместо QUWI для обработки параллельного сжатия.Я только что протестировал его на текстовом файле размером 100 МБ, созданном с помощью механизма цепей Маркова.Я сравнил результаты этого класса с некоторыми другими вариантами.Вот как это выглядит:

uncompressed: 104857600
running 2 cycles, 6 Flavors

System.IO.Compression.GZipStream:  .NET 2.0 builtin
  compressed: 47550941
  ratio     : 54.65%
  Elapsed   : 19.22s

ICSharpCode.SharpZipLib.GZip.GZipOutputStream:  0.86.0.518
  compressed: 37894303
  ratio     : 63.86%
  Elapsed   : 36.43s

Ionic.Zlib.GZipStream:  DotNetZip v1.9.1.5, CompLevel=Default
  compressed: 37896198
  ratio     : 63.86%
  Elapsed   : 39.12s

Ionic.Zlib.GZipStream:  DotNetZip v1.9.1.5, CompLevel=BestSpeed
  compressed: 47204891
  ratio     : 54.98%
  Elapsed   : 15.19s

Ionic.Exploration.ParallelGZipOutputStream: DotNetZip v1.9.1.5, CompLevel=Default
  compressed: 39524723
  ratio     : 62.31%
  Elapsed   : 20.98s

Ionic.Exploration.ParallelGZipOutputStream:DotNetZip v1.9.1.5, CompLevel=BestSpeed
  compressed: 47937903
  ratio     : 54.28%
  Elapsed   : 9.42s

Выводы:

  1. GZipStream, встроенный в .NET, довольно быстрый.Это также не очень эффективно и не настраивается.

  2. "BestSpeed" на ванильном (непараллельном) GZipStream в DotNetZip примерно на 20% быстрее встроенного потока .NETи дает примерно такое же сжатие.

  3. Использование нескольких задач для сжатия может сократить до 45% времени, необходимого на моем двухъядерном ноутбуке (3 ГБ ОЗУ), сравнивая ванильный DotNetZip GZipStream с параллельным.Я предполагаю, что экономия времени будет выше для машин с большим количеством ядер.

  4. Параллельно GZIP стоит платить - кадрирование увеличивает размер сжатого файла примерно на 4%.Это не изменится с количеством используемых ядер.

Полученный файл .gz может быть распакован любым инструментом GZIP.

1 голос
/ 05 мая 2011

Насколько я понимаю, почтовый индекс записывает (или читает) один базовый поток;поэтому мое предположение было бы оглушительным нет ;это не может быть потокобезопасным, если вы говорите об одном базовом потоке.

Однако;отдельные экземпляры, говорящие с отдельными базовыми потоками, должны быть в порядке;и действительно, как правило, легче выполнять отдельные (не связанные) задачи параллельно, чем параллельно одной задаче.

0 голосов
/ 05 мая 2011

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

То, что вы могли бы сделать, это создать потокобезопасный класс Stream посредника (например, шаблон декоратора) и передать его в GZipOutputStream. Этот пользовательский потоковый класс, называемый его ThreadSafeStream, сам по себе будет принимать экземпляр Stream и будет использовать соответствующие механизмы для синхронизации доступа к нему.

Вы создадите один экземпляр GZipOutputStream для каждого потока, и все они будут использовать один и тот же экземпляр оболочки ThreadSafeStream. Я подозреваю, что, вероятно, в методах ThreadSafeStream будет много узких мест, но вы сможете добиться некоторого параллелизма из этого.

...