How to create HttpWebRequest without interrupting

2019-01-28 18:59发布

问题:

I have a bunch of slow functions that are essentially this:

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
    }

}

This works nicely and asynchronously except for the call to WebRequest.Create - this single line freezes the UI thread for several seconds which sort of ruins the purpose of async/await.

I already have this code written using BackgroundWorkers, which works perfectly and never freezes the UI.
Still, what is the correct, idiomatic way to create a web request with respect to async/await? Or maybe there is another class that should be used?

I've seen this nice answer about asyncifying a WebRequest, but even there the object itself is created synchronously.

回答1:

Interestingly, I'm not seeing a blocking delay with WebRequest.Create or HttpClient.PostAsync. It might be something to do with DNS resolution or proxy configuration, although I'd expect these operations to be implemented internally as asynchronous, too.

Anyway, as a workaround you can start the request on a pool thread, although this is not something I'd normally do:

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
    }
}

That would keep the UI responsive, but it might be difficult to cancel it if user wants to stop the operation. That's because you need to already have a WebRequest instance to be able to call Abort on it.

Using HttpClient, cancellation would be possible, something like this:

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);

    // ...
}

With HttpClient, you can also register a httpClient.CancelPendingRequests() callback on the cancellation token, like this.


[UPDATE] Based on the comments: in your original case (before introducing Task.Run) you probably did not need the IProgress<I> pattern. As long as DownloadSomething() was called on the UI thread, every execution step after each await inside DownloadSomething would be resumed on the same UI thread, so you could just update the UI directly in between awaits.

Now, to run the whole DownloadSomething() via Task.Run on a pool thread, you would have to pass an instance of IProgress<I> into it, e.g.:

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 

Note, because DownloadSomething is an async method itself, it is now run as a nested task, which Task.Run transparently unwraps for you. More on this: Task.Run vs Task.Factory.StartNew.

Also check out: Enabling Progress and Cancellation in Async APIs.



回答2:

I think you need to use HttpClient.GetAsync() which returns a task from an HTTP request.

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

It may depend a bit on what you want to return, but the HttpClient has a whole bunch of async methods for requests.