多线程文件压缩

时间:2015-08-16 13:37:22

标签: c# multithreading .net-3.5

我刚开始使用线程, 我想写简单的文件压缩器。它应该创建两个后台线程 - 一个用于读取,另一个用于写入。第一个应该通过小块读取文件并将它们放入Queue,其中int - 是chunkId。第二个线程应该将块出列并按顺序将它们(使用chunkId)写入输出流(该线程在开始时创建的文件)。

我做到了。但我不明白为什么在我的程序结束后我打开我的gziped文件 - 我看,我的块混合了,文件没有先前的订单。

public static class Reader
{
    private static readonly object Locker = new object();

    private const int ChunkSize = 1024*1024;

    private static readonly int MaxThreads;
    private static readonly Queue<KeyValuePair<int, byte[]>> ChunksQueue;
    private static int _chunksComplete;

    static Reader()
    {
        MaxThreads = Environment.ProcessorCount;
        ChunksQueue = new Queue<KeyValuePair<int,byte[]>>(MaxThreads);
    }

    public static void Read(string filename)
    {
        _chunksComplete = 0;

        var tRead = new Thread(Reading) { IsBackground = true };
        var tWrite = new Thread(Writing) { IsBackground = true };

        tRead.Start(filename);
        tWrite.Start(filename);

        tRead.Join();
        tWrite.Join();

        Console.WriteLine("Finished");
    }

    private static void Writing(object threadContext)
    {
        var filename = (string) threadContext;

        using (var s = File.Create(filename + ".gz"))
        {
            while (true)
            {
                var dataPair = DequeueSafe();
                if (dataPair.Value == null)
                    return;

                while (dataPair.Key != _chunksComplete)
                {
                    Thread.Sleep(1);
                }

                Console.WriteLine("write chunk {0}", dataPair.Key);

                using (var gz = new GZipStream(s, CompressionMode.Compress, true))
                {
                    gz.Write(dataPair.Value, 0, dataPair.Value.Length);
                }

                _chunksComplete++;
            }
        }
    }

    private static void Reading(object threadContext)
    {
        var filename = (string) threadContext;

        using (var s = File.OpenRead(filename))
        {
            var counter = 0;
            var buffer = new byte[ChunkSize];
            while (s.Read(buffer, 0, buffer.Length) != 0)
            {
                while (ChunksQueue.Count == MaxThreads)
                {
                    Thread.Sleep(1);
                }

                Console.WriteLine("read chunk {0}", counter);

                var dataPair = new KeyValuePair<int, byte[]>(counter, buffer);

                EnqueueSafe(dataPair);

                counter++;
            }

            EnqueueSafe(new KeyValuePair<int, byte[]>(0, null));
        }
    }

    private static void EnqueueSafe(KeyValuePair<int, byte[]> dataPair)
    {
        lock (ChunksQueue)
        {
            ChunksQueue.Enqueue(dataPair);
        }
    }

    private static KeyValuePair<int, byte[]> DequeueSafe()
    {
        while (true)
        {
            lock (ChunksQueue)
            {
                if (ChunksQueue.Count > 0)
                {
                    return ChunksQueue.Dequeue();
                }
            }

            Thread.Sleep(1);
        }
    } 
}

UPD: 我只能使用.NET 3.5

2 个答案:

答案 0 :(得分:3)

Stream.Read()返回它消耗的实际字节数。用它来限制编写器的块大小。而且,由于涉及并发读写,因此您需要多个缓冲区。 尝试4096作为块大小。

阅读器:

var buffer = new byte[ChunkSize]; 
int bytesRead = s.Read(buffer, 0, buffer.Length);

while (bytesRead != 0)
{  
   ...
   var dataPair = new KeyValuePair<int, byte[]>(bytesRead, buffer); 
   buffer = new byte[ChunkSize];
   bytesRead = s.Read(buffer, 0, buffer.Length);
}

编剧:

 gz.Write(dataPair.Value, 0, dataPair.Key)

PS:通过添加一个空闲数据缓冲池而不是每次分配新的并使用事件(例如ManualResetEvent)来发信号队列是空的,可以提高性能>队列已满,而不是使用Thread.Sleep()

答案 1 :(得分:2)

尽管alexm's answer确实提出了一个非常重要的一点,Stream.Read可以填充buffer的字节数少于您请求的字节数,但您遇到的主要问题是您只有一个{ {1}}你一直在反复使用。

当你的阅读循环读取第二个值时,它会覆盖你传递给队列的byte[]内的byte[]。您必须在循环中使用dataPair 来解决此问题。您还必须记录读入的字节数,并且只写入相同的字节数。

您不需要将buffer = new byte[ChunkSize];保留在对中counter将保持订单,使用对中的Queue来存储记录的字节数就像在alexm的例子中一样。