GC中断和TPL

时间:2012-07-17 08:07:28

标签: .net garbage-collection task-parallel-library

我有一个WCF服务。在服务工作期间,它需要调用两个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,导致调度程序使用非线程池线程,似乎解决了这个问题,但我不想创建这么多新线程 - 这个代码会运行很多(每次运行几次)请求)。

解决此问题的最佳方法是什么?

5 个答案:

答案 0 :(得分:3)

首先让我清理一下本页上的一些误解:

  • 空闲时不会发生GC。它由于分配失败(新),GC.Collect或OS内存压力
  • 而在触发时发生
  • GC可以停止应用程序线程。它不会同时运行(至少在一定时间内)
  • “GC中的%时间”是一个不会在GC之间更改的计数器,这意味着您可能会看到陈旧的值
  • 异步代码对GC问题没有帮助。事实上,它生成更多垃圾(任务,IAsyncResult和其他可能的东西)
  • 在专用线程上运行代码不会阻止它们被停止

如何解决这个问题?

  1. 产生更少的垃圾。附上一个内存分析器(JetBrains很容易使用),看看是什么产生了垃圾以及堆上的内容
  2. 减少堆大小以减少暂停时间(3GB堆可能是由于某些缓存?可能缩小缓存?)
  3. 使用相同的应用程序启动多个ASP.NET站点,连接GC通知以检测GC即将到来,并使一些IIS站点在负载平衡轮换时具有GC(http://blogs.msdn.com/b/jclauzel/archive/2009/12/10/gc-notifications-asp-net-server-workloads.aspx?Redirected=true
  4. 您会注意到没有简单的解决方法。我不知道一个,但如果问题是由GC导致上述问题之一将解决问题。

答案 1 :(得分:1)

我知道你的问题是关于GC的,但我想首先讨论异步实现,然后看看你是否还会遇到同样的问题。

关闭初始实现的示例代码,您现在将浪费三个CPU线程等待I / O:

  • 浪费的第一个线程是执行调用的原始WCF I / O线程。当子任务仍未完成时,它将被Task.WaitAll阻止。
  • 浪费的另外两个线程是用于执行对Service1和Service2的调用的线程池线程

在Service1和Service2的I / O都非常出色的时候,你浪费的三个CPU线程无法用于执行其他工作,GC必须在它们周围提示。

因此,我最初的建议是更改您的WCF方法本身以使用WCF运行时支持的异步编程模型(APM)模式。这通过允许调用您的服务实现的原始WCF I / O线程立即返回其池以便能够为其他传入请求提供服务来解决第一个浪费线程的问题。完成后,您接下来想要从客户端perspectice调用Service1和Service2异步。这将涉及两件事之一:

  1. 生成合同接口的异步版本,同样使用WCF在客户端模型中支持的APM BeginXXX / EndXXX。
  2. 如果这些是您正在谈论的简单REST服务,您还有以下其他异步选择:
    • WebClient::DownloadStringAsync实施(WebClient不是我个人喜爱的API)
    • HttpWebRequest::BeginGetResponse + HttpWebResponse::BeginGetResponseStream + HttpWebRequest::BeginRead
    • 使用新的Web API HttpClient
    • 走向前沿
  3. 将所有这些放在一起,当您等待服务中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;
        }
    }
    

    一旦你完成所有这些,你将在技术上没有CPU线程执行,而Service1和Service2的I / O是未完成的。在这样做时,GC没有线程甚至不必担心大部分时间都在中断。现在唯一一次会发生实际的CPU工作是原始的工作安排,然后继续在ContinueWhenAll上处理任何异常并按下结果。

答案 2 :(得分: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);

这会将每个WaitAll使用的线程池线程数从3减少到1.你仍然不是理想的(0),但你应该看到一个改进。

答案 3 :(得分: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

答案 4 :(得分:0)

执行许多Web请求时,会将大量临时对象加载到托管堆中。当堆确实增长时,GC会在分配新的GC段之前尝试释放一些内存。这是您在工作时看到GCs发生的主要原因。

现在出现了一个有趣的部分:您的GC堆已经是3 GB,并且您在GC堆上还有一些带有短期对象的Web请求。对于死对象,完整的GC将花费大量时间来遍历您当然复杂的对象图(所有3 GB)。在如此高吞吐量的情况下,您将通过线路获取每个请求大量的临时数据,这将迫使许多GC。

此时您受GC约束:应用程序性能不再受您的控制。您可以通过仔细设计数据结构和访问模式来正常解决此问题,但GC时间(我猜是> 95%)将主导您的应用程序性能。

没有简单的方法可以解决这个问题。如果它是一个庞大的复杂系统,通过检查整体内存消耗来缩小GC segmetns可能会很困难。另一种方法可能是产生额外的进程(不是新的AppDomain,因为GC根本不知道AppDomain),并在Web请求中创建短期对象。如果你可以在你的大型服务器进程使用的小进程中计算出有意义的响应,那么可以摆脱这种混乱。如果您的进程确实创建了与原始Web请求相同数量的临时数据,那么您将回到原点,并且一无所获。

从以前的Web请求中重用对象并保持对象池可以减少分配数量可能会有所帮助。

如果你的进程堆中有很多相同的字符串,那么如果它们永远不会被释放,它们可能有助于实习它们。这有助于简化对象图。