記事一覧

ZIP解凍をマルチスレッドで高速化

 

  
目次     



  

ZIP解凍をマルチスレッドで実行する

ハロー、みなさん。エジソンです。

大量のファイルが詰まっているZIPファイルを解凍するのって結構な時間が掛かりますよね?

そんなめちゃ重たい「ZIPファイル」をマルチスレッドで解凍することで高速化できないだろうか…
そんな疑問が頭をもたげてきたので試してみることにしました。

C#で試してみる

言語はC#で.Net Framework4.5です。Windows10 64bitで動作確認済みです。
以下にサンプルプログラムを置きました。


使い方は、ZipParallelUncompress.zipとtest.zipをダウンロードしてexeを実行し、画面上のファイルパスにtest.zipのフルパスを入力し、「スレッドを使用した解凍」または「通常解凍」ボタンを押します。

すると、「実行時間」というテキストに、処理に掛かった時間と出力されたパス(テンポラリファイルに出力)が表示されます。

サンプルなのでUIはいまいちですが、ご了承ください。

ソースの説明

C#には、Parallel#Forという便利なメソッドがあるので、こちらを利用しました。
ZIPを操作するAPIは、System.IO.Compressionという名前空間(コアライブラリ)にあるので、こちらを参照設定しています。

リスト1:Parallel#ForでZIP解凍
private void UncompressParallel(string outDir, int threadCount)
{
    var dirList = new Dictionary(1000);
    var fileList = new List(1000);

    // ZIP書庫を開く
    using (ZipArchive archive = ZipFile.OpenRead(txtArchiveFilePath.Text))
    {

        // 書庫内のファイルとディレクトリを列挙する
        foreach (ZipArchiveEntry entry in archive.Entries)
        {
            if (!string.IsNullOrEmpty(entry.Name))
            {
                fileList.Add(entry.FullName);

                var dir = Path.GetDirectoryName(entry.FullName);
                if (!dirList.ContainsKey(dir))
                {
                    dirList[dir] = true;
                }
            }
        }

        // 書庫内のファイルのディレクトリのみを先に作成する
        foreach (var dir in dirList.Keys)
        {
            var createDir = Path.Combine(outDir, dir);
            if (!Directory.Exists(createDir))
            {
                Directory.CreateDirectory(createDir);
            }
        }

        // 1スレッドあたりの処理数
        var sizePerThread = (int)(fileList.Count / threadCount);

        // 並列処理
        Parallel.For(0, threadCount, id =>
        {
            // 開始インデックスを計算
            var sIndex = id * sizePerThread;
            // 終了インデックスを計算
            var eIndex = (id + 1) * sizePerThread;
            if (id == threadCount - 1)
            {
                // 最後の場合は、ファイルリストの最後まで位置するようにする
                eIndex = fileList.Count;
            }

            for (int i = sIndex; i < eIndex; i++)
            {
                var file = fileList[i];
                var createFile = Path.Combine(outDir, file);

                ZipArchiveEntry entry = archive.GetEntry(file);

                // エントリをファイルとして出力する
                using (var writer = new FileStream(createFile                                             
                                        , FileMode.Create
                                        , FileAccess.ReadWrite
                                        , FileShare.ReadWrite))
                {
                    var b = new byte[1024];
                    var br = 0;

                    // 複数スレッドが同時にZIPアーカイブのエントリにアクセスするとエラーになるのでロックする
                    lock (archive)
                    {
                        // エントリを読み込みファイルに書き込む
                        using (Stream reader = entry.Open())
                        {
                            while ((br = reader.Read(b, 0, b.Length)) > 0)
                            {
                                writer.Write(b, 0, br);
                            }
                        }
                    }
                }
            }
        });
    }
}

長いソースですが要約すると、以下の処理順序になっています。
  1. ZIPを開く
  2. ZIP内のディレクトリとファイル(エントリ)の一覧を取得する
  3. ZIP内の全てのディレクトリを出力ディレクトリに作成する
     (ディレクトリ作成のバッティングがちょっと不安だったので先行して処理してみる)
  4. ZIP内のファイルを抽出し出力ディレクトリに書き込む(マルチスレッド部分)

あえてポイントを上げるなら、ZIPArchiveクラスをlockしている部分でしょうか。当初、lockせずに実行したため、予期せぬエラーに悩まされました。また、リストのインデックスの範囲を計算する点も重要です。

ちなみに、通常のAPIを使った解凍の場合は、ZipFile#ExtractToDirectoryというメソッド一行で実行可能です。

それぞれのアルゴリズムでの計測結果

解凍に使ったZIPファイルは、1MByteのファイルが1000個含まれています。
このファイルに対して、通常の解凍または、マルチスレッド(スレッド数を4)での解凍、それぞれで計測すると、以下のような結果になりました。

アルゴリズム 速度
通常 12.851秒
マルチスレッド 6.109秒

マルチスレッド版の方が約2倍速いようです。長々とソースを書いた甲斐がありました!

マルチコア時代のプログラミング

今回のケースをより抽象的に表現すると…、以下のようにまとめることができます。

並列化できる条件ZIPファイルの並列化で例えると…
処理対象となる要素が膨大
ZIPファイル内のファイル数が多い
要素それぞれで処理する内容は同じ
ZIPファイル内のファイルそれぞれを抽出
要素それぞれは独立しており相互に干渉しない
ZIPファイル内のファイルは相互に干渉していない

このような特性があるケースでは並列化処理は有効なのかもしれませんね。

ハードウェアの進化とともに搭載するCPUが増加し、プログラミング言語上で簡単にマルチスレッドを取り扱うための構文がサポートされている昨今、よりスピーディーに処理したい場面があれば、並列化処理を試す価値は大いにあるのではないかと実感した出来事でした。

関連記事

このエントリーをはてなブックマークに追加

コメント

コメントの投稿

非公開コメント

プロフィール

EZOLABブログへようこそ。
EZOLABは、札幌のソフトウェア会社です。

http://ezolab.co.jp

ezolab