2017-02-09 79 views
0

我想找出使用​​Taskasync/await並行HTTP請求的正確方法。我正在使用HttpClient類,它已經有用於檢索數據的異步方法。如果我只是在foreach循環中調用它並等待響應,則一次只發送一個請求(這很有意義,因爲在await期間,控制權將返回到我們的事件循環中,而不是返回到foreach循環的下一次迭代中)。並行HTTP請求使用System.Net.Http.HttpClient

我來包裹HttpClient看起來這樣

public sealed class RestClient 
{ 
    private readonly HttpClient client; 

    public RestClient(string baseUrl) 
    { 
     var baseUri = new Uri(baseUrl); 

     client = new HttpClient 
     { 
      BaseAddress = baseUri 
     }; 
    } 

    public async Task<Stream> GetResponseStreamAsync(string uri) 
    { 
     var resp = await GetResponseAsync(uri); 
     return await resp.Content.ReadAsStreamAsync(); 
    } 

    public async Task<HttpResponseMessage> GetResponseAsync(string uri) 
    { 
     var resp = await client.GetAsync(uri); 
     if (!resp.IsSuccessStatusCode) 
     { 
      // ... 
     } 

     return resp; 
    } 

    public async Task<T> GetResponseObjectAsync<T>(string uri) 
    { 
     using (var responseStream = await GetResponseStreamAsync(uri)) 
     using (var sr = new StreamReader(responseStream)) 
     using (var jr = new JsonTextReader(sr)) 
     { 
      var serializer = new JsonSerializer {NullValueHandling = NullValueHandling.Ignore}; 
      return serializer.Deserialize<T>(jr); 
     } 
    } 

    public async Task<string> GetResponseString(string uri) 
    { 
     using (var resp = await GetResponseStreamAsync(uri)) 
     using (var sr = new StreamReader(resp)) 
     { 
      return sr.ReadToEnd(); 
     } 
    } 
} 

由我們的事件循環中調用的代碼是

public async void DoWork(Action<bool> onComplete) 
{ 
    try 
    { 
     var restClient = new RestClient("https://example.com"); 

     var ids = await restClient.GetResponseObjectAsync<IdListResponse>("/ids").Ids; 

     Log.Info("Downloading {0:D} items", ids.Count); 
     using (var fs = new FileStream(@"C:\test.json", FileMode.Create, FileAccess.Write, FileShare.Read)) 
     using (var sw = new StreamWriter(fs)) 
     { 
      sw.Write("["); 

      var first = true; 
      var numCompleted = 0; 
      foreach (var id in ids) 
      { 
       Log.Info("Downloading item {0:D}, completed {1:D}", id, numCompleted); 
       numCompleted += 1; 
       try 
       { 
        var str = await restClient.GetResponseString($"/info/{id}"); 
        if (!first) 
        { 
         sw.Write(","); 
        } 

        sw.Write(str); 

        first = false; 
       } 
       catch (HttpException e) 
       { 
        if (e.StatusCode == HttpStatusCode.Forbidden) 
        { 
         Log.Warn(e.ResponseMessage); 
        } 
        else 
        { 
         throw; 
        } 
       } 
      } 

      sw.Write("]"); 
     } 

     onComplete(true); 
    } 
    catch (Exception e) 
    { 
     Log.Error(e); 
     onComplete(false); 
    } 
} 

我已經嘗試不同的方法涉及Parallel.ForEachLinq.AsParallel,幷包裹了一把循環的全部內容在Task中。

回答

4

其基本思想是保持跟蹤所有異步任務,並一次等待它們。要做到這一點最簡單的方法是將您的foreach體內提取到一個單獨的異步方法,做這樣的事情:

var tasks = ids.Select(i => DoWorkAsync(i)); 
await Task.WhenAll(tasks); 

這樣,各個任務分別發出(還是按順序,但不等待爲了完成I/O),並且你在同一時間等待它們。

請注意,您還需要進行一些配置 - 默認情況下,HTTP會被限制爲僅允許兩個同時連接到同一臺服務器。

+0

因此,您是說因爲HTTP庫的異步調用如何工作,我可以同時啓動所有任務,而無需擔心同時發送數千個請求的垃圾郵件。 –

+0

查看接受的答案在這裏:http://stackoverflow.com/questions/19102966/parallel-foreach-vs-task-run-and-task-whenall –

+0

@AustinWagner默認情況下,是的。 HTTP限制是HTTP規範的一部分,因此在技術上禁用(或放寬)它違反了規範。也就是說,我們生活在不同的時代 - 多個併發請求並不像HTTP最初設計時那樣糟糕。無論如何,如果你希望(顯着)限制速度,那麼你可能也想實現自己的節流 - 否則你只是在浪費一堆內存來處理並行處理,而不是將其轉換爲流 - 假設你當然,並不需要所有的迴應。 – Luaan