【问题标题】:How to create HttpWebRequest without interrupting async/await?如何在不中断异步/等待的情况下创建 HttpWebRequest?
【发布时间】:2026-02-19 17:05:02
【问题描述】:

我有一堆慢速函数,本质上是这样的:

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 线程几秒钟,这会破坏 async/await 的目的。

我已经使用BackgroundWorkers 编写了这段代码,它运行良好且不会冻结 UI。
不过,创建与 async/await 相关的 Web 请求的正确惯用方法是什么?或者应该使用另一个类?

我已经看到 this nice answer 关于异步化 WebRequest,但即使在那里对象本身也是同步创建的。

【问题讨论】:

    标签: c# httpwebrequest .net-4.5 async-await


    【解决方案1】:

    有趣的是,我没有看到 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


    [更新] 基于 cmets:在您的原始情况下(在引入 Task.Run 之前)您可能不需要 IProgress&lt;I&gt; 模式。只要在 UI 线程上调用了 DownloadSomething()DownloadSomething 内每个 await 之后的每个执行步骤都将在同一个 UI 线程上恢复,因此您可以直接在 awaits 之间更新 UI。

    现在,要在池线程上通过Task.Run 运行整个DownloadSomething(),您必须将IProgress&lt;I&gt; 的实例传递给它,例如:

    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.Running 来调整整个事情,而不仅仅是函数内部的一个位(这样只有一个额外的线程可以做自己的await 业务,并且每个函数的胆量都不在乎他们在哪个线程上)。这不起作用,因为我通过 IProgress 访问 UI 元素并报告进度,并且跨线程访问失败。是否有可能以某种方式使传递给 Task.Run() 的最外层 lambda 使用正确的同步上下文,还是我必须将 Invokes 放入我的 Report 方法中?
    • 这基本上是我采用您的原始答案后所拥有的。唉,以这种方式传递相同的取消令牌并不会神奇地将调用编组到 UI 线程。由于对 UI 的跨线程访问,progressIndicator.Report() 内部仍然失败。我已经通过在报告函数中检查InvokeRequired 并根据需要检查Invokeing 来修复它,但这感觉很遗憾(与自动发生这种情况的后台工作人员相比)和泄漏的抽象。我可以保持原样,但我真的很想学习如何在不破坏抽象的情况下使其工作。
    • @GSerg,这很奇怪。在传递它之前,您实际上是在 UI 线程上创建Progress&lt;T&gt; 吗? Progress&lt;T&gt; 构造函数捕获它在其上创建的线程的当前同步上下文,因此保证在相同的同步上下文(在您的情况下为 UI 线程)调用进度回调。在创建Progress&lt;T&gt; 的实例之前尝试以下操作:MessageBox.Show(SynchronizationContext.Current.GetType().Name)。你看到了什么?
    • @GSerg,不要继承,只需使用 stock 类并给它自己的进度回调:new Progress&lt;int&gt;(Report)。它在正确的上下文中调用Report。完整示例可通过我的答案末尾的链接获得。
    • 我认为Task.Run() 中的 lambda 应该返回创建的请求,而不是设置变量。
    【解决方案2】:

    我认为您需要使用从 HTTP 请求返回任务的 HttpClient.GetAsync()。

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

    这可能取决于您想要返回的内容,但 HttpClient 有一大堆用于请求的异步方法。

    【讨论】:

    • 不幸的是,这给await PostAsync 行带来了同样的问题。在等待完成之前挂起停止,所以我假设在 HttpClient 内部有一个 HttpWebRequest 正在同步创建和异步读取,这正是我手动执行的操作。
    • 啊,我明白了,好点子。我想如果你依赖于请求的结果,你总是需要等到它完成。您可以继续处理其他内容,但无法与结果进行交互。顺便说一句,如果您需要发出多个请求并按照它们返回的顺序处理数据,那么有一个非常好的 InCompletionOrder() 函数。 gist.github.com/mswietlicki/6112628