none
Parallel.ForEachでのOutOfMemoryの追い方は? RRS feed

  • 質問

  • 複数(数万オーダ)のjpegファイルに対する処理をしています。

    通常のforeachでは数時間かかるため Parallel.ForEachを利用して、高速化を図っているのですが、うまくいきません。数千のあたりでOutOfMemoryの例外が発生します。

     

    ・Image、Bitmap、FileStreamはusing文で使用する。

    ・newしたメモリで必要なくなった物はnullを入れる。

    ・GC.Collect()をメモリ操作する前、return前に挿入する。

     

    と、メモリリーク対策はしたのですが、あまり役にはたっていないようです。

    パフォーマンスモニターでログを取ってみると、以下のアドレスの図のようになります。

    http://f.hatena.ne.jp/sauberwind/20101220140906

    見たところ、メモリ使用量は極端な上下なく、スレッド数が極端に増加している感があります。

    このような場合、どのように追いかけていけばよいでしょうか?

    環境はVC#2010 Expressです。

     

    #Parallel.ForEach→foreachでは正常に終了します。

    2010年12月20日 5:14

すべての返信

  • 発生している例外はOutOfMemoryなので、メモリ不足が発生していることには違いないと思います。

    パラレル処理は、システムの負荷状況を見て並列で処理するかの判断をすると思いますので、

    別途、並列処理を実行してみてメモリが不足するかどうかまでは判定してないと思います。

    そして、単純ループ(1件ずつ)の場合はメモリ不足が発生しないが、並列処理になると

    その並列時分の合計でメモリ不足になるようなケースが、数千件のあたりで発生するのでは

    ないでしょうか。

    OutOfMemoryExceptionが起こるか前に、十分なメモリリソースがあるかチェックできる

    MemoryFailPointクラスがあります。

    MemoryFailPoint クラス

    これで、実際にメモリ不足になるようなケースが発生しているか確認してはどうでしょうか。

    あとスレッド数がどんどん増えるということですが、ループの開始と終了にカウンタの

    インクリメントとデクリメントを行う行を追加して、実際のスレッド数の動向を調査してみては

    どうかなと思いました。

    スレッド数まで管理したいのであれば、(メモリ不足にならないような数をMAXとしたいなど)

    別の仕組み、例えばThread.Poolとかでやる方法などもあると思います。

    2010年12月20日 6:40
  • スレッド数に上限を設けたいだけ、ということなら Parallel.ForEach() の引数に new ParallelOptions { MaxDegreeOfParalelleism = 100; } などとすれば解決しませんかね?

    デフォルトでは、並列スレッド数は無制限になっているので、100万個の要素を与えたら100万個のスレッドが生成され、既定個数づつスケジューリングされて実行されるのはないか、、、と。

    2010年12月20日 8:49
  • スレッド数上限に関してですけど、K.Takaokaさんの言うとおりオプションでありましたね、失礼しました。

    http://msdn.microsoft.com/ja-jp/library/system.threading.tasks.paralleloptions.maxdegreeofparallelism.aspx

    なので、後半のThreadPoolで実装する必要はないですね。

     

    2010年12月20日 9:03
  • とは言え、メモリ不足に陥ってまでスレッドを増やすとも思えないです。

    グラフを見ると後半、急にスレッド数が増加しています。プログラムを見せてもらわないことにはわかりませんが、ループ間に何らかの依存関係があり、デッドロックのような状態に陥り、処理が進まないから仕方がなくスレッド数を増加させた、という動作なのかもしれません。

    というわけで、ループ間に明示的・暗黙的な依存関係があったりしませんか?

    2010年12月20日 9:55
  • みなさん、回答ありがとうございます。

    とりあえず、Parallel.Invoke版を作って、今動かしています。

    メモリ・スレッド数とも安定した値がでてますので、こちらはとりあえず良さそうです。

     

    Parallel.ForEachに制限をかけられるのは初耳で、こちらは試してみようかと思います。

    #自分の環境では大丈夫だけれども、よそにもって行ったらダメ、とかありそうですね。

     

    佐祐理さんの疑問もごもっともだと思います。

    写真ファイルのパス(string)をループの対象に、Exifデータを抜き出して、ConcurrentBag<T>に格納しています。個々の処理に依存関係はありません。

    ConcurrentBagの要素数=対象ファイル数ですので、これが大きすぎるのか、ということも考えましたが、シングルスレッド版ではOutOfMemory例外が発生しないのでこれは違うでしょうね…

    参考になるかもしれませんので、下に関連するコードをつけておきます。

     

    Parallel.ForEach<string>(targetFiles, GetExifRecords);
    
    ↓
    
      private static void GetExifRecords(string targetFile, ParallelLoopState state)
      {
       //スレッドキャンセルを受ける
       if (state.IsStopped)
       {
        return;
       }
    
       using (ExifDataContainer exifDat = new ExifDataContainer(targetFile,bOverWriteThumbnail))
       {
        ExifRecord record = exifDat.ToPhotoExifTable();
        records.Add(record);
        record = null;
        System.GC.Collect();
       }
       //ダイアログに経過数を通知する。
       backGroundWorker.ReportProgress(records.Count<ExifRecord>());
      }
    
    
    
    

     

    2010年12月20日 10:23
  • >  System.GC.Collect();
    > backGroundWorker.ReportProgress(records.Count<ExifRecord>());

    この2行を削除するとスレッド増えないと思ってしまうのだけど、どうだろう?


    GC Thread と GUI Thread とコレクションの待ち合わせで、みんなデッドロックに近い状態になっているだけではないか?と思います。

    GC.Collect() は重たい処理ですし、無駄に呼び出してもヒープの世代があがってどんどん効率が悪くなりかねません。どうしても GC が割り込めるような構造にならないかぎり呼び出さないほうがよいでしょう。

    次に同期待ちをつくっていそうなのは Count() で、同期コレクションの場合には、どれかのスレッドが Count() を呼び出している間は他のスレッドは Add() できない上に、コレクションの実装によっては Count() は非常に重たい処理になります。時間の経過とともに加速度的にスレッド数が残ってしまうのは、コレクションの中身が増えて Count() に時間がかかるようになり、それにあわせて残るスレッドの数が増えていくのかと思います。

    このような並列性のある処理のフィードバックを行う場合、進捗を表示する側が圧倒的に処理頻度が少ないため、BackgroundWorker を扱うなら、適度な処理間隔で進捗描画側から状態を取得するのがベターな解決になるかと思います。(私は処理数がすくなくてもそういう設計にしてることが多いですが)

    • 編集済み K. Takaoka 2010年12月21日 3:26 Count と スレッド数について1文ほど追加
    2010年12月20日 23:46
  • sauberwind さんのブログの 11/4 時点のコードでは、次の Image は解放されていませんね。
    「Image、Bitmap、FileStreamはusing文で使用する」と書かれているので、最新コードでは直ってるかもしれませんけど。

    Image picFile = Image.FromStream(fs, false, false);

    他に思ったことですが、
    record = null;
    は今回の場合は無意味ですし、
    System.GC.Collect();
    はパフォーマンス上、しない方が絶対いいと思います。
    ReportProgress も効率は良くなさそうに思います。UIスレッド側で3秒間隔ぐらいで進捗を再表示するぐらいでもいいんじゃないかなと思いました。

    それと records がどんなものかはわかりませんが、records.Add はスレッドセーフな操作でしょうか?
    List<T> でしたらそうではないのでダメです。
    OutOfMemory とは関係ないかもしれないですけど。。

    追記:↓佐祐理さんご指摘の点、そうですね。ちゃんと見てませんでした(〃д〃)

    • 編集済み TH01 2010年12月21日 2:41 追記
    2010年12月21日 1:28
  • それと records がどんなものかはわかりませんが、records.Add はスレッドセーフな操作でしょうか?
    List<T> でしたらそうではないのでダメです。

    ConcurrentBag<T>と書かれているので大丈夫だと思います。その他同意見です。GC.Collect()とBackGroundWorker.ReportProgress()はやりすぎでしょうね。

    blogを読むと、Array.Copy()してからBitConverterで読み取っているのも無駄な手順に感じました。

    2010年12月21日 2:10
  • みなさん、ありがとうございます。

    record=null; system.GC.Collect; は「リークしている可能性を少しでも排除したい」というだけでしたので削除します。

    ReportProgress は特に「重い」という印象も無かったので、こちらは別スレッドでダイアログに経過を通知する形にしようかと思っています。(タスクの切り替えは重い、とどこかで読んだ気がするので、とりあえず)

    Imageに関してはリークしていると思われた段階で解放するように変更していました。 Array.Copyに関しても、リーク疑いの段階で書き直しています。 そのへん踏まえまして、

    1. GC.Collectはやらない。
    2. ReportProgressは別タスクに移し、500msec更新とする。 と、
    3. targetfilesを100個単位に分割し、recordsの量を一定に押さえ込む。(→プログレスダイアログ用にはintカウンタをlockで更新する)
    4. DBファイルを更新タイミングのみ開く を実装しようと考えています。

    結果は、また報告します。

    #ForEachを20スレッドに押さえるのは、メモリ例外になりました。10スレッド版では正常に終了しています。

     

    #改行等変更しました。4.実装してなかった。

    2010年12月21日 7:08
  • 書き直して試してみました。

    結論から言うと、ReportProgress,GCが直接の原因ではないようです。

    75スレッドに増えたあたりで落ちています。

    ForEachに制約を設ける、同時処理数を100→50で通りそうではありますが、ほかの環境だとどうなるか、というところが少しいやらしいですね。

     

          List<string> targets;              //対象ファイル
    
          //画像ファイルを100個単位にして処理する。
          for (int i = 0; (i * 100) < targetFiles.Count(); i++)
          {
    
            if (targetFiles.Count() - (i + 1) * 100 > 0)  //対象ファイルが残り100個より大であれば
            {
              targets = targetFiles.GetRange(
                i * 100,            //0→  100→  200
                100               //99→  199→  299
                );
            }
            else  //対象が残り100個以下なら
            {
              int remain = targetFiles.Count() - 100 * i;
              targets = targetFiles.GetRange(i * 100, remain);
            }
    
            //cancel受信処理
            if (backGroundWorker.CancellationPending == true)  //プログレスダイアログのcancelが押された。
            {
              e.Cancel = true;                //キャンセル受け取り
              beCanceled = true;               //ForEachのキャンセルフラグを立てる。
              goto receive_cancel;
            }
    
            records = new ConcurrentBag<ExifRecord>();
    
            //Exifレコードを取得する
            Parallel.ForEach<string>(targets, GetExifRecords);
            if (records.Count() != 0)              //データベースに差し込むレコードがあれば
            {
              context.ExifRecord.InsertAllOnSubmit<ExifRecord>(records);
              context.SubmitChanges();
            }
          }
    
        receive_cancel: //キャンセルされた
          return;
        }
    
        //
        //Exifレコードを取得する(Parallel.ForEachされる処理)
        //
        private static void GetExifRecords(string targetFile, ParallelLoopState state)
        {
          if ((backGroundWorker.CancellationPending == true) //プログレスダイアログからCancelが押された
            ||(beCanceled == true))             //親スレッドがCancelを受信した。
          {
            state.Break();
            return;
          }
    
    
          using (ExifDataContainer exifDat = new ExifDataContainer(targetFile, gbOverWriteThumbnails))
          {
            ExifRecord record = exifDat.ToPhotoExifTable();
            records.Add(record);
    
            object o = new object();
            lock (o)
            {
              nProgressNum++;
            }
          }
        }
    
    

    2010年12月21日 13:21
  • まず

    object o = new object();
    lock (o)
    {
      nProgressNum++;
    }

    はなんのロックにもなっていません。うまくインクリメントできないこともあるでしょう。ただしこれ自体がOutOfMemoryの原因にはならないでしょうが、これと同様のことをしている個所で、なんらか空回りして予定以上にループを回してしまったりすることもあるかと思います。

    Interlocked.Increment ( ref nProgressNum );

    かな。先のコメントにも「ループ間に明示的・暗黙的な依存関係があったりしませんか?」と書きましたが、こういった問題がいろいろと残っているんでしょうね。

     

    蛇足ですが、100個ずつグループ分けするのはGroupBy() でできます。(100の商でグループ分けするので、0~99、100~199、200~255のように分かれます。)

    var i = 0;
    IEnumerable<IEnumerable<string>> groupedTargetFiles = targetFiles.GroupBy( _ => i++ / 100 );

    2010年12月21日 15:24
  • こういった問題がいろいろと残っているんでしょうね。

    お恥ずかしい。

    一応コードをおいておきました。今回指摘いただいた内容は反映未です。

    https://docs.google.com/leaf?id=0BxdI-qE-v9O7MzQ1MGZhNGMtODk2YS00ODAyLWE5MTEtODdlYjNmYTJjYWM0&sort=name&layout=list&num=50

     

    #念のため書いておきますが、「デバッグしてください」というつもりではなく、

    #興味を持たれた方の参考になれば、と思ってのことです。

     

    後段、GroupByに関しても、ありがとうございました。

    C#の世界の広さに少し気後れ気味になっています。

     

    2010年12月21日 16:17
  • とりあえず、recordsをループごとに上書きしているので、過去のrecordsに対してAdd()した内容は消えてなくなりそうです。

    あとExifDataContainerコンストラクタの中にもGC.Collect()が入っていました。OutOfMemoryが出てしまったせいで気になるのでしょうが、.NETはいちいちnull代入しなくてもメモリ解放はきちんと行われます。なので例えば、ExifDataContainerをIDisposableにする必要はありません。

    今のところ、私は根本の原因を見つけることができませんでした。PropertyItem[]をIEnumerable<PropertyItem>としてLINQアクセスしている辺りは効率悪いなぁとは思いますが、OutOfMemoryの原因にはならなさそうですし。

    もう1つ原因ではありませんが、SQL Server Compact 3.5 SP1を使用されているんですね。4.0 CTP1 に気になる記述があります。

    Virtual Memory Reduction – The virtual memory that Compact uses has been reduced in Compact 4.0 CTP1. The visible difference is that if an application tries to open 40 to 50 simultaneous connections in SQL Server Compact 3.5 SP2, an ‘out of virtual memory’ exception will be hit.

    今回、複数コネクションを張っていなさそうなので該当しないとは思いますが。

    2010年12月21日 22:56
  • こんにちは。

     

    null代入、IDisposeに関しては「本来GCされるはずなのに」、と考える物すべてに、つけていった名残です。

    OutOfMemoryで時間がたつと落ちる

    →メモリリークをうたがう

    →auto変数の肥大化を疑う

    という手順でやってました。ここまでやったあとでデータを取ってスレッド数・メモリ量を見たというのは手順がおかしかったですね。

    本筋とは離れて申し訳ないんですが、

    佐祐理さんの書かれた

    PropertyItem[]をIEnumerableとして…のところがよくわかりませんでした。直接picFile.PropertyItemsをクエリの対象にした方がよい、ということであってますか?

    targetFiles.GroupBy( _ => i++ / 100 );ですが、ラムダ式での_が何をさすかというところが、文法を調べていたのですが、解が見つかっていません。何を示しているのでしょうか? i++/100を要素ごとに行い、それをキーとしてグループ化、というところまでは読めたのですが。

    (追記)_は 変数名とのことを教えていただきました。

    ありがとうございます。

     

    2010年12月22日 4:23