Sam*_*Sam 4 c# performance c#-5.0 cancellationtokensource .net-4.5
我有一些代码使用来自一个特定主机的内容执行批量HTML页面处理.它尝试使用大量(~400)同时发出的HTTP请求HttpClient.我认为同时连接的最大数量受到限制ServicePointManager.DefaultConnectionLimit,所以我没有应用自己的并发限制.
在将所有请求异步发送到HttpClient使用后Task.WhenAll,可以使用CancellationTokenSource和取消整个批处理操作CancellationToken.可以通过用户界面查看操作的进度,并且可以单击按钮以执行取消.
对CancellationTokenSource.Cancel()块的调用大约需要5到30秒.这会导致用户界面冻结.怀疑是因为该方法正在调用注册取消通知的代码.
HttpClient似乎已经排除了过多的请求本身.CancellationTokenSource.Cancel()在非UI线程中执行方法调用.这不太好用; 在大多数其他人完成任务之前,任务实际上并未运行.我认为async该方法的一个版本可以很好地工作,但我找不到一个.另外,我的印象是它适合在UI线程中使用该方法.class Program
{
private const int desiredNumberOfConnections = 418;
static void Main(string[] args)
{
ManyHttpRequestsTest().Wait();
Console.WriteLine("Finished.");
Console.ReadKey();
}
private static async Task ManyHttpRequestsTest()
{
using (var client = new HttpClient())
using (var cancellationTokenSource = new CancellationTokenSource())
{
var requestsCompleted = 0;
using (var allRequestsStarted = new CountdownEvent(desiredNumberOfConnections))
{
Action reportRequestStarted = () => allRequestsStarted.Signal();
Action reportRequestCompleted = () => Interlocked.Increment(ref requestsCompleted);
Func<int, Task> getHttpResponse = index => GetHttpResponse(client, cancellationTokenSource.Token, reportRequestStarted, reportRequestCompleted);
var httpRequestTasks = Enumerable.Range(0, desiredNumberOfConnections).Select(getHttpResponse);
Console.WriteLine("HTTP requests batch being initiated");
var httpRequestsTask = Task.WhenAll(httpRequestTasks);
Console.WriteLine("Starting {0} requests (simultaneous connection limit of {1})", desiredNumberOfConnections, ServicePointManager.DefaultConnectionLimit);
allRequestsStarted.Wait();
Cancel(cancellationTokenSource);
await WaitForRequestsToFinish(httpRequestsTask);
}
Console.WriteLine("{0} HTTP requests were completed", requestsCompleted);
}
}
private static void Cancel(CancellationTokenSource cancellationTokenSource)
{
Console.Write("Cancelling...");
var stopwatch = Stopwatch.StartNew();
cancellationTokenSource.Cancel();
stopwatch.Stop();
Console.WriteLine("took {0} seconds", stopwatch.Elapsed.TotalSeconds);
}
private static async Task WaitForRequestsToFinish(Task httpRequestsTask)
{
Console.WriteLine("Waiting for HTTP requests to finish");
try
{
await httpRequestsTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("HTTP requests were cancelled");
}
}
private static async Task GetHttpResponse(HttpClient client, CancellationToken cancellationToken, Action reportStarted, Action reportFinished)
{
var getResponse = client.GetAsync("http://www.google.com", cancellationToken);
reportStarted();
using (var response = await getResponse)
response.EnsureSuccessStatusCode();
reportFinished();
}
}
Run Code Online (Sandbox Code Playgroud)

为什么取消阻止这么久?还有,我做错了什么或者做得更好?
在非UI线程中执行CancellationTokenSource.Cancel()方法调用.这不太好用; 在大多数其他人完成任务之前,任务实际上并未运行.
这告诉我的是,你可能正在遭受'线程池耗尽',这是你的线程池队列中有很多项目(来自HTTP请求完成),需要一段时间来完成它们.取消可能阻止某些线程池工作项执行,它不能跳到队列的头部.
这表明您确实需要从您的考虑清单中选择选项1.限制自己的工作,以便线程池队列保持相对较短.无论如何,这对于整体应用响应非常有用.
我最喜欢的限制异步工作的方法是使用Dataflow.像这样的东西:
var block = new ActionBlock<Uri>(
async uri => {
var httpClient = new HttpClient(); // HttpClient isn't thread-safe, so protect against concurrency by using a dedicated instance for each request.
var result = await httpClient.GetAsync(uri);
// do more stuff with result.
},
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 20, CancellationToken = cancellationToken });
for (int i = 0; i < 1000; i++)
block.Post(new Uri("http://www.server.com/req" + i));
block.Complete();
await block.Completion; // waits until everything is done or canceled.
Run Code Online (Sandbox Code Playgroud)
作为替代方案,您可以使用在TaskCreationOptions.LongRunning中传递的Task.Factory.StartNew,以便您的任务获得一个新线程(不隶属于线程池),这将允许它立即启动并从那里调用Cancel.但你应该解决线程池耗尽问题.