2014-01-08 34 views
2

我有一大堆的慢功能,這些功能基本上是這樣的:如何在不中斷異步/等待的情況下創建HttpWebRequest?

private async Task<List<string>> DownloadSomething() 
{ 
    var request = System.Net.WebRequest.Create("https://valid.url"); 

    ... 

    using (var ss = await request.GetRequestStreamAsync()) 
    { 
     await ss.WriteAsync(...); 
    } 

    using (var rr = await request.GetResponseAsync()) 
    using (var ss = rr.GetResponseStream()) 
    { 
     //read stream and return data 
    } 

} 

這很好地工作和異步除了調用WebRequest.Create - 這一行凍結UI線程幾秒鐘哪種類型遺址的目的異步/ AWAIT。

我已經使用BackgroundWorker s編寫了這段代碼,它完美地工作,永遠不會凍結UI。
仍然,什麼是正確的,慣用的方式來創建一個關於異步/等待的Web請求?或者也許應該使用另一個類?

我見過this nice answer關於異步WebRequest,但即使在那裏對象本身是同步創建的。

回答

6

有趣的是,我沒有看到阻擋延遲WebRequest.CreateHttpClient.PostAsync。這可能與DNS解析或代理配置有關,儘管我希望這些操作也可以在內部以異步方式實現。

不管怎樣,作爲一個解決辦法你就可以開始在一個線程池的要求,雖然這不是我通常做的:

private async Task<List<string>> DownloadSomething() 
{ 
    var request = await Task.Run(() => { 
     // WebRequest.Create freezes?? 
     return System.Net.WebRequest.Create("https://valid.url"); 
    }); 

    // ... 

    using (var ss = await request.GetRequestStreamAsync()) 
    { 
     await ss.WriteAsync(...); 
    } 

    using (var rr = await request.GetResponseAsync()) 
    using (var ss = rr.GetResponseStream()) 
    { 
     //read stream and return data 
    } 
} 

這將保持UI響應,但也可能是如果用戶想要停止操作,很難到cancel。這是因爲您需要已經有一個WebRequest實例才能夠對其調用Abort

使用HttpClient,取消將是可能的,這樣的事情:

private async Task<List<string>> DownloadSomething(CancellationToken token) 
{ 
    var httpClient = new HttpClient(); 

    var response = await Task.Run(async() => { 
     return await httpClient.PostAsync("https://valid.url", token); 
    }, token); 

    // ... 
} 

隨着HttpClient,你也可以在取消標記註冊一個回調httpClient.CancelPendingRequests(),如this


[更新]根據意見:在您的原始案例中(在介紹 Task.Run之前),您可能不需要 IProgress<I>模式。只要在UI線程上調用 DownloadSomething(),在 DownloadSomething內部的每個 await之後的每個執行步驟都將在同一UI線程上恢復,因此您可以直接在 awaits之間更新UI。

現在,要在池線程上通過Task.Run運行整個DownloadSomething(),您必須將IProgress<I>的實例傳遞給它,例如,:

private async Task<List<string>> DownloadSomething(
    string url, 
    IProgress<int> progress, 
    CancellationToken token) 
{ 
    var request = System.Net.WebRequest.Create(url); 

    // ... 

    using (var ss = await request.GetRequestStreamAsync()) 
    { 
     await ss.WriteAsync(...); 
    } 

    using (var rr = await request.GetResponseAsync()) 
    using (var ss = rr.GetResponseStream()) 
    { 
     // read stream and return data 
     progress.Report(...); // report progress 
    } 
} 

// ... 

// Calling DownloadSomething from the UI thread via Task.Run: 

var progressIndicator = new Progress<int>(ReportProgress); 
var cts = new CancellationTokenSource(30000); // cancel in 30s (optional) 
var url = "https://valid.url"; 
var result = await Task.Run(() => 
    DownloadSomething(url, progressIndicator, cts.Token), cts.Token); 
// the "result" type is deduced to "List<string>" by the compiler 

注意,因爲DownloadSomething是一個async方法本身,現在是運行爲嵌套任務,Task.Run透明解開你。更多關於此:Task.Run vs Task.Factory.StartNew

也檢出:Enabling Progress and Cancellation in Async APIs

+0

這樣工作。然而,我嘗試通過Task.Run來適應這個問題,而不僅僅是一個函數內部的一點點(這樣只有一個額外的線程可以完成自己的「await」事務,而且每個函數的內部函數不在乎他們在什麼線程)。這是行不通的,因爲我通過'IProgress'重新推進了進程,在那裏訪問UI元素,並且在跨線程訪問時失敗。是否有可能以某種方式使最外層的lambda傳遞給'Task.Run()'使用正確的同步上下文,還是必須將'Invoke'放在我的'Report'方法中? – GSerg

+0

@GSerg,我發佈了更新。這就是你所追求的? – Noseratio

+0

這實際上是我採用原始答案後的結果。唉,以這種方式傳遞相同的取消標記不會奇蹟般地調用UI線程。由於跨UI訪問UI,因此仍在progressIndicator.Report()內失敗。我已經通過在報告函數中檢查「InvokeRequired」並根據需要檢查「Invoke」來解決這個問題,但這感覺很遺憾(與發生這種情況的背景工作人員相比)和漏洞抽象。我可以保持原樣,但我真的很想學習如何在不打破抽象的情況下使其工作。 – GSerg

0

我認爲你需要使用HttpClient.GetAsync(),它從HTTP請求中返回一個任務。

http://msdn.microsoft.com/en-us/library/hh158912(v=vs.110).aspx

這可能取決於你想要什麼回報了一下,但HttpClient的有一大堆的請求異步方法。

+1

不幸的是,這在'awaAs PostAsync'行上給出了相同的掛起。 hang在await完成之前停止,所以我假設'HttpClient'內有一個'HttpWebRequest'同步創建和異步讀取,這正是我手動完成的。 – GSerg

+0

啊,我看,好點。我想如果你依賴於請求的結果,你總是需要等到它完成。您可以繼續處理其他內容,但無法與結果互動。 作爲一個方面,如果您需要製作多個請求並按照它們返回的順序處理數據,那麼會有一個非常好的InCompletionOrder()函數。 https://gist.github.com/mswietlicki/6112628 – dougajmcdonald