如何在不中断async/await的情况下创建HttpWebRequest?

GSe*_*erg 2 c# httpwebrequest async-await .net-4.5

我有一堆很慢的函数,基本上是这样的:

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
    }

}
Run Code Online (Sandbox Code Playgroud)

除了调用之外,这很好地和异步地工作WebRequest.Create- 这一行将UI线程冻结几秒钟,这会破坏async/await的目的.

我已经使用BackgroundWorkers 编写了这个代码,它完美运行并且永远不会冻结UI.
但是,创建与async/await相关的Web请求的正确,惯用方法是什么?或者也许应该使用另一个类?

我已经看到了关于异步a的好答案WebRequest,但即使在那里,对象本身也是同步创建的.

nos*_*tio 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
    }
}
Run Code Online (Sandbox Code Playgroud)

这将使UI保持响应,但如果用户想要停止操作,则可能很难取消它.那是因为你需要有一个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);

    // ...
}
Run Code Online (Sandbox Code Playgroud)

使用HttpClient,您还可以httpClient.CancelPendingRequests()在取消令牌上注册回调,如下所示.


[更新]基于评论:在您的原始案例中(在介绍之前Task.Run)您可能不需要该IProgress<I>模式.只要DownloadSomething()在UI线程上调用,每个await内部之后的每个执行步骤都DownloadSomething将在同一个UI线程上恢复,因此您可以直接在两者之间更新UI awaits.

现在,要在池线程上运行整个DownloadSomething()via Task.Run,您必须将一个实例传递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 
Run Code Online (Sandbox Code Playgroud)

注意,因为它本身DownloadSomething就是一个async方法,所以它现在作为嵌套任务运行,它可以Task.Run透明地为你解开.更多相关信息:Task.Run vs Task.Factory.StartNew.

另请参阅:在异步API中启用进度和取消.

  • @GSerg,不要继承,只需使用stock类并给它自己的进程回调:`new Progress <int>(Report)`.**将*在正确的上下文中调用`Report`.我的答案结尾处的链接提供了完整的示例. (2认同)
  • 我认为`Task.Run()`中的lambda应该*返回*创建的请求,而不是设置变量. (2认同)