在多个线程上并行调用ICsharpCode.SharpZipLib是否安全

far*_*ast 4 .net zip multithreading sharpziplib

我们目前正在使用压缩ICsharpCode.SharpZipLib库的GZipOutputStream类.我们从一个线程做到这一点.

我想将输入数据流拆分为块并并行压缩它们.我担心这个库可能有一些静态内部将被多个线程覆盖,因此破坏了生成的流.

任何想法将不胜感激.

Che*_*eso 11

这是一个非常有趣的问题.压缩是高度CPU密集型的,依赖于大量的搜索和比较.因此,当您拥有多个具有无阻碍内存访问权限的CPU时,想要并行化它是非常合适的.

ParallelDeflateOutputStream在DotNetZip库中有一个叫做你所描述的类的类.这个课程在这里记录.

它只能用于压缩 - 无需减压.它也是严格意义上的输出流 - 你无法read压缩.考虑到这些约束,它基本上是一个DeflateOutputStream,它在内部使用多个线程.

它的工作方式:它将传入的流分解为块,然后将每个块放入一个单独的工作线程中进行单独压缩.然后它将所有这些压缩流合并到一个有序流中.

假设流维护的"块"大小是N个字节.当调用者调用Write()时,数据被缓冲到桶或块中.在Stream.Write()方法内部,当第一个"桶"已满时,它调用ThreadPool.QueueUserWorkItem,将桶分配给工作项.对流的后续写入开始填充下一个桶,当已满时,再次Stream.Write()调用QUWI.每个工作线程使用"Flush Type" Sync(参见deflate规范)压缩其存储桶,然后将其压缩的blob标记为输出.然后重新排序这些各种输出(因为块n不一定在块n + 1之前被压缩),并写入捕获输出流.在写入每个桶时,它被标记为空,准备在下一个桶中重新填充Stream.Write().每个块必须使用刷新类型的Sync进行压缩,以便通过简单的串联重新组合,以使组合的字节流成为合法的DEFLATE流.最后的块需要Flush type = Finish.

此流的设计意味着调用者不需要使用多个线程进行编写. 调用者只是像往常一样创建流,就像用于输出的vanilla 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);
            }                    
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它被设计用于DotNetZip ZipFile类,但它可以作为独立的压缩输出流使用.可以用任何充气器对得到的物流进行脱氢(膨胀?).结果完全符合规范.

流是可调整的.您可以设置它使用的缓冲区的大小以及并行度.它不会在没有绑定的情况下创建存储桶,因为对于会导致内存不足的大流(gb scale等).因此,对于桶的数量有固定的限制,因此可以支持并行度.

在我的双核机器上,与标准的DeflateStream相比,这个流类几乎使大(100mb和更大)文件的压缩速度翻了一番.我没有任何更大的多核机器,所以我无法进一步测试.权衡是并行实现使用更多的CPU和更多的内存,并且由于我上面描述的同步成帧,压缩效率稍差(大文件减少1%).性能优势将根据输出流的I/O吞吐量以及存储是否可以跟上并行压缩器线程而变化.


警告:
这是一个DEFLATE流,而不是GZIP.有关差异,请阅读RFC 1951(DEFLATE)RFC 1952(GZIP).

但是如果你真的需要gzip,那么这个流的来源是可用的,所以你可以查看它,也许可以为自己获得一些想法.GZIP实际上只是DEFLATE之上的一个包装器,带有一些额外的元数据(比如Adler校验和,等等 - 参见规范).在我看来,建立一个ParallelGzipOutputStream,并不是很困难,但它也可能不是微不足道的.

对我来说最棘手的部分是让Flush()和Close()的语义正常工作.


编辑

为了好玩,我构建了一个ParallelGZipOutputStream,它基本上完成了我上面描述的GZip.它使用.NET 4.0的Tasks代替QUWI来处理并行压缩.我刚刚在通过马尔可夫链引擎生成的100mb文本文件上测试了它.我将该类的结果与其他一些选项进行了比较.这是它的样子:

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
Run Code Online (Sandbox Code Playgroud)

结论:

  1. 内置于.NET的GZipStream非常快.它也不是很有效,而且不可调.

  2. DotNetZip中的vanilla(非并行化)GZipStream上的"BestSpeed"比.NET内置流快约20%,并提供相同的压缩.

  3. 使用多个任务进行压缩可以减少我的双核笔记本电脑(3GB RAM)所需时间的45%,比较香草DotNetZip GZipStream和并行的.我认为拥有更多内核的机器可以节省更多时间.

  4. 并行GZIP需要成本 - 框架会将压缩文件的大小增加大约4%.这不会随着使用的核心数量而变化.

生成的.gz文件可以通过任何GZIP工具解压缩.


Bri*_*eon 0

在对类进行编码时,确保所有静态成员都是线程安全的是标准做法。所以我认为你不太可能因为这个问题而遇到问题。当然,如果您计划在不同线程中使用相同的 GZipOutputStream成员,那么这肯定会出现问题,因为该类的实例成员不是线程安全的。

您可以做的是创建一个线程安全的中间人类Stream(想想装饰器模式)并将其传递给GZipOutputStream. 这个自定义流类(称为 )ThreadSafeStream本身会接受一个Stream实例,并使用适当的机制来同步对其的访问。

您将为GZipOutputStream每个线程创建一个实例,并且它们都将共享同一个ThreadSafeStream包装器实例。我怀疑这些方法可能会存在很多瓶颈ThreadSafeStream,但您应该能够从中获得一些并行性。