GC中断和TPL

Dor*_*oby 9 .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 });
Run Code Online (Sandbox Code Playgroud)

大部分时间这种方法都可以,但偶尔我会看到执行时间出现峰值,第一项任务需要几秒钟才能开始.看着perfmon,我意识到这正是GC发生的时候.显然,GC优先于运行我的任务.这是不可接受的,因为延迟对我来说非常重要,我更喜欢GC在请求之间而不是在请求中间发生.

我尝试以不同的方式进行此操作,而不是旋转我自己的任务,我使用了WebClient.DownloadStringTask.

return webClient.DownloadStringTask(urlWithParmeters).ContinueWith(t => ProcessResponse(clientQuery, t.Result),
                                                                           TaskContinuationOptions.ExecuteSynchronously);
Run Code Online (Sandbox Code Playgroud)

这没有用; GC现在在任务开始后运行,但在继续之前运行.再说一次,我猜它认为系统现在处于空闲状态,所以现在是开始GC的好时机.只是,我负担不起延迟.

使用TaskCreationOptions.LongRunning,导致调度程序使用非线程池线程,似乎解决了这个问题,但我不想创建这么多新线程 - 这个代码将运行很多(每个请求多次).

克服这个问题的最佳方法是什么?

Dre*_*rsh 1

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

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

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

一直以来,当 Service1 和 Service2 的 I/O 未完成时,您浪费的三个 CPU 线程无法用于执行其他工作,GC 必须小心翼翼地绕过它们。

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

  1. 生成其契约接口的异步版本,这些版本再次使用 WCF 在客户端模型中支持的 APM BeginXXX/EndXXX。
  2. 如果您正在使用这些简单的 REST 服务,则您还有以下其他异步选择:
    • WebClient::DownloadStringAsync实现(WebClient不是我个人最喜欢的 API)
    • HttpWebRequest::BeginGetResponse+ HttpWebResponse::BeginGetResponseStream+HttpWebRequest::BeginRead
    • 利用新的 Web API 走在前沿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;
    }
}
Run Code Online (Sandbox Code Playgroud)

一旦一切就绪,从技术上讲,当 Service1 和 Service2 的 I/O 未完成时,您将不会执行任何 CPU 线程。这样做时,GC 无需担心大多数时间会出现中断的线程。现在发生实际 CPU 工作的唯一时间是工作的原始调度,然后继续在ContinueWhenAll 上处理任何异常并处理结果。