如何使用HttpClient加速任务处理

4

我有一个流程,需要向服务器发出约100个http api调用并处理结果。我编写了这个commandexecutor,它构建了一系列命令,然后异步运行它们。进行大约100次调用和解析结果需要超过1分钟的时间。使用浏览器进行1次请求可以在约100毫秒内获得响应。你可能认为100次调用大约需要10秒钟。我相信我做错了什么,这应该能更快。

 public static class CommandExecutor
 {
    private static readonly ThreadLocal<List<Command>> CommandsToExecute =
        new ThreadLocal<List<Command>>(() => new List<Command>());
    private static readonly ThreadLocal<List<Task<List<Candidate>>>> Tasks =
        new ThreadLocal<List<Task<List<Candidate>>>>(() => new List<Task<List<Candidate>>>());

    public static void ExecuteLater(Command command)
    {
        CommandsToExecute.Value.Add(command);
    }

    public static void StartExecuting()
    {
        foreach (var command in CommandsToExecute.Value)
        {
            Tasks.Value.Add(Task.Factory.StartNew<List<Candidate>>(command.GetResult));
        }

        Task.WaitAll(Tasks.Value.ToArray());
    }

    public static List<Candidate> Result()
    {
        return Tasks.Value.Where(x => x.Result != null)
                          .SelectMany(x => x.Result)
                          .ToList();
    }
}

我正在传递到这个列表中的命令会创建一个新的httpclient,调用该客户端上的getasync方法并传入一个url,将字符串响应转换为对象,然后填充一个字段。
    protected void Initialize()
    {
        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
    }

    protected override void Execute()
    {
        Initialize();

        var task = _httpClient.GetAsync(string.Format(Url, Input));
        Result = ConvertResponseToObjectAsync(task).Result;
        Result.ForEach(x => x.prop = value);
    }

    private static Task<Model> ConvertResponseToObjectAsync(Task<HttpResponseMessage> task)
    {
        return task.Result.Content.ReadAsAsync<Model>(
           new MediaTypeFormatter[]
           {
                 new Formatter()
           });
    }

您能发现我的瓶颈并提出如何加速的建议吗?

编辑:进行这些更改后,时间缩短到了4秒。

protected override void Execute()
    {
        Initialize();

        _httpClient.GetAsync(string.Format(Url, Input))
        .ContinueWith(httpResponse => ConvertResponseToObjectAsync(httpResponse)
        .ContinueWith(ProcessResult));
    }

    protected void ProcessResult(Task<Model> model)
    {
        Result = model.Result;
        Result.ForEach(x => x.prop = value);
    }

还要检查您实际能够进行多少并行请求。有一些默认限制可能会限制吞吐量。 - Alexei Levenkov
dotTrace(我是新手)显示63%在 system.threading.monitor.wait 上。我将添加一些调试打印语句以查看每个语句的执行速度。我已经对代码进行了一些更改,如果这些更改没有帮助,我将更新问题。 - Steve
1
你应该使用ContinueWith(或使用async/await)来避免在ConvertResponseToObjectAsync(它会阻塞直到输入任务完成)和Execute中阻塞等待任务完成。通常情况下,你应该使用'await',如果无法使用await,则使用ContinueWith,这样就不会阻塞等待任务完成。 - James Manning
谢谢@JamesManning,我在其中一个地方修复了它。您能否对我添加的编辑进行评论? - Steve
我必须承认,我不清楚在处理任务时为什么要使用ThreadLocal - 任务通常不是线程关联的,所以我认为任务集合只是一个“单例”,而不是每个线程。是否有多个线程调用StartExecuting? - James Manning
显示剩余7条评论
2个回答

5

停止创建新的HttpClient实例。每次您销毁一个HttpClient实例时,它都会关闭TCP/IP连接。创建一个HttpClient实例,并重复使用它处理每个请求。HttpClient可以在多个不同线程上同时进行多个请求。


2
避免在ConvertResponseToObjectAsync中和Execute中使用task.Result。而是将它们链接到原始的GetAsync任务上,使用ContinueWith方法。
目前,Result会阻止当前线程的执行,直到其他任务完成。然而,您的线程池很快会因等待其他任务而被挤满,这些任务没有任何地方可以运行。最终(等待一秒钟后),线程池将添加一个额外的线程来运行,所以最终会完成,但效率不高。
作为一般原则,除了在任务连续性中,应避免访问Task.Result。
作为奖励,您可能不想使用ThreadLocalStorage。ThreadLocalStorage在访问它的每个线程上存储一个存储在其中的项目实例。在这种情况下,看起来您需要一种线程安全但共享的存储形式。我建议在这种情况下使用ConcurrentQueue。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接