有趣的是,我沒有看到阻擋延遲WebRequest.Create
或HttpClient.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。
這樣工作。然而,我嘗試通過Task.Run來適應這個問題,而不僅僅是一個函數內部的一點點(這樣只有一個額外的線程可以完成自己的「await」事務,而且每個函數的內部函數不在乎他們在什麼線程)。這是行不通的,因爲我通過'IProgress'重新推進了進程,在那裏訪問UI元素,並且在跨線程訪問時失敗。是否有可能以某種方式使最外層的lambda傳遞給'Task.Run()'使用正確的同步上下文,還是必須將'Invoke'放在我的'Report'方法中? – GSerg
@GSerg,我發佈了更新。這就是你所追求的? – Noseratio
這實際上是我採用原始答案後的結果。唉,以這種方式傳遞相同的取消標記不會奇蹟般地調用UI線程。由於跨UI訪問UI,因此仍在progressIndicator.Report()內失敗。我已經通過在報告函數中檢查「InvokeRequired」並根據需要檢查「Invoke」來解決這個問題,但這感覺很遺憾(與發生這種情況的背景工作人員相比)和漏洞抽象。我可以保持原樣,但我真的很想學習如何在不打破抽象的情況下使其工作。 – GSerg