Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
717 views
in Technique[技术] by (71.8m points)

c# - How to create HttpWebRequest without interrupting async/await?

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...