ASP.NET Web API 客户端进度消息处理程序发布任务卡在 WinForm 应用程序中

本文关键字:任务 应用程序 WinForm 布任务 API Web NET 客户端 程序 消息处理 ASP | 更新日期: 2023-09-27 17:56:41

我正在使用 MS ASP.NET Web API 客户端库中的HttpClientProgressMessageHandler

我很高兴在控制台应用程序中毫无问题地修补了它,但现在在 WinForm 应用程序中,"发布"任务只是简单地卡在.Wait().Result上。

以下是我非常简单的测试应用程序的完整列表。按钮 1 工作正常,按钮 2 每次在调用postTask.Result时都会冻结。为什么?

以 4.0 或 4.5 为目标没有区别。控制台应用程序中的相同代码没有问题。

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Handlers;
using System.Windows.Forms;
namespace WindowsFormsApplication13
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        private ProgressMessageHandler _progressHandler = new ProgressMessageHandler();
        private const string SERVICE_URL = "http://www.google.com.au";
        private HttpClient GetClient(bool includeProgressHandler = false)
        {
            var handlers = new List<DelegatingHandler>();
            if (includeProgressHandler)
            {
                handlers.Add(_progressHandler);
            }
            var client = HttpClientFactory.Create(handlers.ToArray());
            client.BaseAddress = new Uri(SERVICE_URL);
            return client;
        }
        private void PostUsingClient(HttpClient client)
        {
            var postTask = client.PostAsJsonAsync("test", new
            {
                Foo = "Bar"
            });
            var postResult = postTask.Result;
            MessageBox.Show("OK");
        }
        private void button1_Click(object sender, EventArgs e)
        {
            using (var client = GetClient())
            {
                PostUsingClient(client);
            }
        }
        private void button2_Click(object sender, EventArgs e)
        {
            using (var client = GetClient(true))
            {
                PostUsingClient(client);
            }
        }
    }
}

更新

好的,所以看起来这是我的问题。对于 .NET 4.5,显而易见的解决方案是,正如@StephenCleary所建议的那样,让异步/等待模式从PostAsJsonAsync调用一直渗透到按钮单击处理程序中。更像这样的东西:

private Task<HttpResponseMessage> PostUsingClient(HttpClient client)
{
    return client.PostAsJsonAsync("test", new
    {
        Foo = "Bar"
    });
}
private async void button2_Click(object sender, EventArgs e)
{
    var client = GetClient(true);
    var response = await PostUsingClient(client);
}

现在我的问题是在 .NET 4.0 中获得等效的解决方案(当然是出于遗留原因)。一个近似的是在按钮单击处理程序中使用延续,麻烦是(再次出于遗留原因),我实际上希望按钮单击处理程序阻止直到异步返回。我使用 4.0 兼容的 yield 运算符找到了一些创造性的解决方案,但它们感觉有点混乱。相反,我能想到的最简单的替代方案是:

private void button2_Click(object sender, EventArgs e)
{
    var result = Task.Run(() => { return PostUsingClient(client); }).Result;
}

我无法想象这是性能最高的实现,坦率地说,它仍然感觉很笨拙。我能做得更好吗?

ASP.NET Web API 客户端进度消息处理程序发布任务卡在 WinForm 应用程序中

任何时候你试图混合同步和异步代码,你最终会得到一些混乱。(我有一篇博客文章解释了为什么这种死锁发生在 Windows 窗体上,而不是控制台应用程序上)。

最好的解决方案是 让它都async .

(再次出于遗留原因),我实际上希望按钮单击处理程序阻止,直到异步返回。

考虑一些替代方案。是否可以禁用async单击处理程序开头的按钮并在结束时重新启用它?这是一种相当常见的方法。

也许如果您描述为什么希望按钮单击处理程序阻止(在另一个问题中),我们可以建议替代解决方案。

我能设计的最简单的替代方案是[使用Task.Run]。我无法想象这是性能最高的实现,坦率地说,它仍然感觉很笨拙。

这是一个与其他解决方案一样好的解决方案。如果调用层次结构中的所有await都使用 ConfigureAwait(false),则可以直接调用PostUsingClient ResultResult的一个问题是它将任何异常包装在AggregateException中,因此您的错误处理也变得更加复杂。

我使用以下函数来解决此问题(仅用于改造旧版)。

public T ExecuteSync<T>(Func<Task<T>> function) {
            return new TaskFactory(TaskScheduler.Default).StartNew((t) => function().Result, TaskContinuationOptions.None).Result; ;
        }
private void button2_Click(object sender, EventArgs e)
{
    var client = GetClient(true);
    var response = ExecuteSync(() => PostUsingClient(client));
}

这对我有用,因为它保证了在尝试同步运行异步内容时,它将始终使用线程池任务调度程序完成,而不是基于同步上下文的任务调度程序。 您遇到的问题是,由于您在延续上调用 GetAsync,因此它位于任务内,默认情况下,所有新任务都继承创建任务的任务计划程序。 因此,您最终会尝试在 UI 线程上运行 GetAsync,但随后会阻止 UI 线程。 当您不在任务中运行时,默认行为是使用默认任务调度程序(即线程池调度程序)执行 GetAsync。 (至少到目前为止是我的理解)

另一方面,您可能不希望在每个请求上都创建新的 HttpClient。 在应用程序范围或至少在窗体范围创建一个并重复使用它。 每次释放 HttpClient 时,它都会强制尝试关闭底层 TCP 连接,从而有效地消除了 HTTP 1.1 的默认保持活动行为的好处。