调用Web服务时,C#Tasks和Continuations的最有效配置是什么?

时间:2013-03-04 18:19:37

标签: c# .net performance task-parallel-library threadpool

我正在创建一个与实时Web API对话的网络客户端。

客户端必须每秒进行多次不同的调用,并将Task<TResult>反馈给每个客户端组件,以便客户端决定是否阻止:

public Task<TResult> Execute<TResult>(IOperation<TResult> operation);

进行API调用的过程如下:

  1. 将(小的,小于1KB)请求序列化为Json
  2. 使用HttpClient
  3. 发送请求
  4. 成功后,反序列化为TResult(Json的大小可能只有几百KB,但通常要小得多)并返回
  5. 在我的测试中,选择在任务工作流程中包含每个步骤的位置(以及因此在哪个线程上)对性能产生重大影响。

    到目前为止我发现的最快的设置是这个(半伪代码,为简洁省略了省略的通用类型参数):

    // serialize on main thread
    var requestString = JsonConvert.SerializeObject(request);
    // create message - omitted
    var post = Task.Factory.StartNew(() => this.client.SendAsync(requestMessage)).Unwrap();
    return post.ContinueWith(response =>
                {
                    var jsonString = response.Result.Content.ReadAsStringAsync();
                    return JsonConvert.DeserializeObject(jsonString.Result);
                });
    

    最慢的是这个设置,整个过程在一个任务中执行:

    return Task.Factory.StartNew((request) => 
                {
                    var requestString = JsonConvert.SerializeObject(request);
                    // create message - omitted
                    var post = client.SendAsync(requestMessage);
                    var jsonString = post.Result.Content.ReadAsStringAsync();
                    return JsonConvert.DeserializeObject(jsonString.Result);
                })
    

    我原以为最后一种方法可能是最快的,因为你为每个请求创建了一个后台线程。我的假设是,由于阻塞调用,这不允许TPL最有效地使用可用线程。

    那么,是否有一个关于应该在任务中应该做什么以及应该在它之外或者在延续中应该做什么的一般规则?

    在这个具体案例中,我可以尝试进一步的优化吗?

2 个答案:

答案 0 :(得分:7)

您根本不需要使用Task.Factory.StartNew,因为SendAsync已经返回Task

var post = this.client.SendAsync(requestMessage);
return post.ContinueWith(response =>
        {
            var jsonString = response.Result.Content.ReadAsStringAsync();
            return JsonConvert.DeserializeObject(jsonString.Result);
        });

这实际上会更有效率,因为它根本不需要ThreadPool线程。

请注意,您可以使用async / await进一步优化此项(以使响应保持异步):

var response = await this.client.SendAsync(requestMessage);
var jsonString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject(jsonString);

这也可以通过TPL延续来编写,但这需要从ReadAsStringAsync返回(解包)任务,然后在其上发布新的延续以获得最终字符串。

答案 1 :(得分:2)

因此,首先,在发送初始请求时没有理由使用StartNew。你已经有了一个任务,在后台线程中启动任务是不必要的开销。

所以这个:

var post = Task.Factory.StartNew(() => this.client.SendAsync(requestMessage)).Unwrap();

可以成为:

var post = this.client.SendAsync(requestMessage);

接下来,在这两种情况下,您都会阻止ReadAsStringAsync方法的结果,而不是异步处理它。这正在嚼掉另一个线程池线程,只是坐在那里什么都不做。

取而代之的是:

return post.ContinueWith(response =>
    {
        return response.Result.Content.ReadAsStringAsync()
            .ContinueWith(t => JsonConvert.DeserializeObject(t.Result));
    }).UnWrap();