我有一个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,导致调度程序使用非线程池线程,似乎解决了这个问题,但我不想创建这么多新线程 - 这个代码会运行很多(每次运行几次)请求)。
解决此问题的最佳方法是什么?
答案 0 :(得分:3)
首先让我清理一下本页上的一些误解:
如何解决这个问题?
您会注意到没有简单的解决方法。我不知道一个,但如果问题是由GC导致上述问题之一将解决问题。
答案 1 :(得分:1)
我知道你的问题是关于GC的,但我想首先讨论异步实现,然后看看你是否还会遇到同样的问题。
关闭初始实现的示例代码,您现在将浪费三个CPU线程等待I / O:
在Service1和Service2的I / O都非常出色的时候,你浪费的三个CPU线程无法用于执行其他工作,GC必须在它们周围提示。
因此,我最初的建议是更改您的WCF方法本身以使用WCF运行时支持的异步编程模型(APM)模式。这通过允许调用您的服务实现的原始WCF I / O线程立即返回其池以便能够为其他传入请求提供服务来解决第一个浪费线程的问题。完成后,您接下来想要从客户端perspectice调用Service1和Service2异步。这将涉及两件事之一:
WebClient::DownloadStringAsync
实施(WebClient
不是我个人喜爱的API)HttpWebRequest::BeginGetResponse
+ HttpWebResponse::BeginGetResponseStream
+ HttpWebRequest::BeginRead
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;
}
}
一旦你完成所有这些,你将在技术上没有CPU线程执行,而Service1和Service2的I / O是未完成的。在这样做时,GC没有线程甚至不必担心大部分时间都在中断。现在唯一一次会发生实际的CPU工作是原始的工作安排,然后继续在ContinueWhenAll上处理任何异常并按下结果。
答案 2 :(得分:0)
我建议你重新考虑德鲁的回答。完全异步的系统将是理想的。
但是如果您想更改更少的代码,可以使用FromAsync
代替StartNew
(这需要Service1
和Service2
的异步代理):
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;
}
答案 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请求中重用对象并保持对象池可以减少分配数量可能会有所帮助。
如果你的进程堆中有很多相同的字符串,那么如果它们永远不会被释放,它们可能有助于实习它们。这有助于简化对象图。