9

私はWCFサービスを持っています。サービスの作業中に、2 つの Web サービスを呼び出す必要があります。したがって、次のようなコードがあります。

var task1 = Task.Factory.StartNew(() => _service1.Run(query));
var task2 = Task.Factory.StartNew(() => _service2.Run(query));
Task.WaitAll(new[] { task1 , task2 });

ほとんどの場合、これで問題なく動作しますが、最初のタスクの開始に数秒かかるなど、実行時間のスパイクがときどき見られました。perfmon を見て、これがまさに GC が発生していた時期であることに気付きました。どうやら、GC はタスクの実行よりも優先度が高かったようです。待ち時間は私にとって非常に重要であり、リクエストの途中ではなく、リクエスト間で GC が発生することを望んでいるため、これは受け入れられません。

これを別の方法で実行しようとしましたが、自分のタスクを回転させる代わりに、WebClient.DownloadStringTaskを使用しました。

return webClient.DownloadStringTask(urlWithParmeters).ContinueWith(t => ProcessResponse(clientQuery, t.Result),
                                                                           TaskContinuationOptions.ExecuteSynchronously);

これは役に立ちませんでした。GC は、タスクの開始後、継続前に実行されるようになりました。繰り返しますが、システムが現在アイドル状態になっていると判断したので、GC を開始するのに適した時期です。ただ、待ち時間が許せません。

スケジューラがスレッドプール以外のスレッドを使用するようにする TaskCreationOptions.LongRunning を使用すると、これは解決されるようですが、私はそれほど多くの新しいスレッドを作成したくありません。このコードは大量に実行されます (要求ごとに数回)。

この問題を克服する最善の方法は何ですか?

4

5 に答える 5

3

最初に、このページに見られるいくつかの誤解を整理させてください。

  • アイドル状態では GC は実行されません。割り当ての失敗 (新規)、GC.Collect、または OS のメモリ不足が原因でトリガーされた場合に発生します。
  • GC はアプリケーション スレッドを停止できます。同時に実行されない (少なくとも一定時間)
  • 「% time in GC」は、GC 間で変化しないカウンターであり、古い値が表示される可能性があることを意味します
  • 非同期コードは GC の問題には役立ちません。実際、より多くのガベージ (タスク、IAsyncResult、およびおそらくその他のもの) が生成されます。
  • 専用スレッドでコードを実行しても、それらの停止は妨げられません

これを修正する方法は?

  1. ごみの発生を少なくします。メモリ プロファイラーをアタッチし (JetBrains は使いやすい)、何がガベージを生成しているか、何がヒープ上にあるかを確認します。
  2. 一時停止時間を短縮するためにヒープ サイズを減らします (3 GB のヒープは、おそらく何らかのキャッシングによるものですか? キャッシュを縮小するのでしょうか?)
  3. 同じアプリで複数の ASP.NET サイトを開始し、GC 通知を接続して GC が来るのを感知し、GC が行われている間、IIS サイトの一部を負荷分散ローテーションから外します ( http://blogs.msdn.com/b /jclauzel/archive/2009/12/10/gc-notifications-asp-net-server-workloads.aspx?Redirected=true )

簡単な修正がないことに気付くでしょう。私にはわかりませんが、GC が原因で問題が発生している場合は、上記のいずれかで問題が解決します。

于 2012-07-23T09:00:17.963 に答える
1

あなたの質問がGCに関するものであることは知っていますが、最初に非同期実装について話し、次に同じ問題が引き続き発生するかどうかを確認したいと思います。

最初の実装のサンプルコードから離れると、今はI/Oを待っている3つのCPUスレッドを無駄にすることになります。

  • 無駄になる最初のスレッドは、呼び出しを実行している元のWCF I/Oスレッドです。子タスクがまだ未処理である間は、Task.WaitAllによってブロックされます。
  • 無駄になっている他の2つのスレッドは、Service1とService2への呼び出しを実行するために使用しているスレッドプールスレッドです。

Service1とService2へのI/​​Oが未処理である間は、無駄にしている3つのCPUスレッドを使用して他の作業を実行することはできず、GCはそれらを回避する必要があります。

したがって、最初に推奨するのは、WCFメソッド自体を変更して、WCFランタイムでサポートされている非同期プログラミングモデル(APM)パターンを使用することです。これにより、サービス実装を呼び出す元のWCF I / Oスレッドをすぐにプールに戻して、他の着信要求を処理できるようにすることで、最初の無駄なスレッドの問題が解決されます。それが済んだら、次に、クライアントの観点からService1とService2への呼び出しも非同期にします。これには、次の2つのいずれかが含まれます。

  1. コントラクトインターフェイスの非同期バージョンを生成します。これも、WCFがクライアントモデルでもサポートするAPM BeginXXX/EndXXXを使用します。
  2. これらが話している単純なRESTサービスである場合、他に次の非同期の選択肢があります。
    • WebClient::DownloadStringAsync実装(WebClient個人的には私のお気に入りのAPIではありません)
    • HttpWebRequest::BeginGetResponse+ HttpWebResponse::BeginGetResponseStream+HttpWebRequest::BeginRead
    • 新しいWebAPIで最先端を行くHttpClient

これらすべてをまとめると、サービスでService1とService2からの応答を待っている間、無駄なスレッドは発生しません。WCFクライアントルートを使用した場合、コードは次のようになります。

// Represents a common contract that you talk to your remote instances through
[ServiceContract]
public interface IRemoteService
{
   [OperationContract(AsyncPattern=true)]
   public IAsyncResult BeginRunQuery(string query, AsyncCallback asyncCallback, object asyncState);
   public string EndRunQuery(IAsyncResult asyncResult);

}

// Represents your service's contract to others
[ServiceContract]
public interface IMyService
{
   [OperationContract(AsyncPattern=true)]
   public IAsyncResult BeginMyMethod(string someParam, AsyncCallback asyncCallback, object asyncState);
   public string EndMyMethod(IAsyncResult asyncResult);
}

// This would be your service implementation
public MyService : IMyService
{
    public IAsyncResult BeginMyMethod(string someParam, AsyncCallback asyncCallback, object asyncState)
    {
        // ... get your service instances from somewhere ...
        IRemoteService service1 = ...;
        IRemoteService service2 = ...;

        // ... build up your query ...
        string query = ...;

        Task<string> service1RunQueryTask = Task<string>.Factory.FromAsync(
            service1.BeginRunQuery,
            service1.EndRunQuery,
            query,
            null);

        // NOTE: obviously if you are really doing exactly this kind of thing I would refactor this code to not be redundant
        Task<string> service2RunQueryTask = Task<string>.Factory.FromAsync(
            service2.BeginRunQuery,
            service2.EndRunQuery,
            query,
            null);

        // Need to use a TCS here to retain the async state when working with the APM pattern
        // and using a continuation based workflow in TPL as ContinueWith 
        // doesn't allow propagation of async state
        TaskCompletionSource<object> taskCompletionSource = new TaskCompletionSource<object>(asyncState);

        // Now we need to wait for both calls to complete before we process the results
        Task aggregateResultsTask = Task.ContinueWhenAll(
             new [] { service1RunQueryTask, service2RunQueryTask })
             runQueryAntecedents =>
             {
                 // ... handle exceptions, combine results, yadda yadda ...
                 try
                 {
                     string finalResult = ...;

                     // Propagate the result to the TCS
                     taskCompletionSoruce.SetResult(finalResult);
                 }
                 catch(Exception exception)
                 {
                     // Propagate the exception to the TCS 
                     // NOTE: there are many ways to handle exceptions in antecedent tasks that may be better than this, just keeping it simple for sample purposes
                     taskCompletionSource.SetException(exception);
                 }
             });

         // Need to play nice with the APM pattern of WCF and tell it when we're done
         if(asyncCallback != null)
         {
             taskCompletionSource.Task.ContinueWith(t => asyncCallback(t));
         }

         // Return the task continuation source task to WCF runtime as the IAsyncResult it will work with and ultimately pass back to use in our EndMyMethod
         return taskCompletionSource.Task;
    }

    public string EndMyMethod(IAsyncResult asyncResult)
    {
        // Cast back to our Task<string> and propagate the result or any exceptions that might have occurred
        return ((Task<string>)asyncResult).Result;
    }
}

すべてが整ったら、Service1とService2のI / Oが未処理である間、技術的にはCPUスレッドは実行されません。これを行う際に、GCがほとんどの時間中断することを心配する必要さえあるスレッドはありません。現在実際にCPU作業が行われるのは、作業の元のスケジュールと、例外を処理して結果をマッサージするContinueWhenAllでの継続のみです。

于 2012-07-19T23:51:25.693 に答える
0

多くの Web リクエストを実行すると、非常に多くの一時オブジェクトがマネージ ヒープに読み込まれます。ヒープが大きくなる間、GC は新しい GC セグメントを割り当てる前にメモリを解放しようとします。これが、作業中に GC が発生する主な理由です。

ここで興味深い部分があります。GC ヒープは既に 3 GB であり、GC ヒープに追加で存続期間の短いオブジェクトを含む Web リクエストがいくつかあります。完全な GC では、確かに複雑なオブジェクト グラフ (すべて 3 GB) を走査して死んだオブジェクトを探すのに多くの時間がかかります。このような高スループットのシナリオでは、リクエストごとにかなりの量の一時データがネットワーク経由で取得され、多くの GC が強制されます。

この時点で、GC バウンドになります。アプリケーションのパフォーマンスは、もは​​や制御できません。これは通常、データ構造とアクセス パターンを注意深く設計することで修正できますが、GC 時間がアプリケーションのパフォーマンスの大部分 (95% を超えると思います) を支配します。

これを回避する簡単な方法はありません。全体的なメモリ消費量をチェックして GC セグメントを小さくするのは、大規模で複雑なシステムの場合は難しいかもしれません。別の方法として、追加のプロセスを生成し (GC は AppDomain をまったく認識しないため、新しい AppDomain ではありません)、Web 要求で存続期間の短いオブジェクトを作成することもできます。次に、大きなサーバー プロセスで使用される小さなプロセスで意味のある応答を計算できれば、この混乱から抜け出すことができます。プロセスが元の Web 要求と同じ量の一時データを作成する場合、振り出しに戻り、何も得られません。

以前の Web 要求からのオブジェクトを再利用し、割り当ての数を減らすためにオブジェクトのプールを準備しておくと役立つ場合があります。

プロセス ヒープに同一の文字列が多数ある場合、それらが解放されない場合はそれらをインターンすると役立つ場合があります。これは、オブジェクト グラフを単純化するのに役立ちます。

于 2012-07-26T12:32:27.343 に答える
0

ドリューの答えを再考することをお勧めします。完全な非同期システムが理想的です。

ただし、変更するコードを減らしたい場合は、FromAsync代わりに を使用できます (これにはとのStartNew非同期プロキシが必要です)。Service1Service2

var task1 = Task.Factory.FromAsync(_service1.BeginRun, _service1.EndRun, query, null);
var task2 = Task.Factory.FromAsync(_service2.BeginRun, _service2.EndRun, query, null);
Task.WaitAll(task1, task2);

これにより、使用されるスレッド プール スレッドの数がWaitAll3 から 1 に減少します。まだ理想 (0) には達していませんが、改善が見られるはずです。

于 2012-07-22T21:37:59.983 に答える
0

これを試してみたいと思うかもしれませんが、問題を少し先送りすることができます:

try
{
    GCSettings.LatencyMode = GCLatencyMode.LowLatency;

    // Generation 2 garbage collection is now
    // deferred, except in extremely low-memory situations

    var task1 = Task.Factory.StartNew(() => _service1.Run(query));
    var task2 = Task.Factory.StartNew(() => _service2.Run(query));
    Task.WaitAll(new[] { task1 , task2 });
}
finally
{
    // ALWAYS set the latency mode back
    GCSettings.LatencyMode = oldMode;
}

クレジットはhttps://stackoverflow.com/users/153498/mgbowenに与える必要があります

于 2012-07-26T04:52:19.510 に答える