ASP.NET Web API 客户端进度消息处理程序发布任务卡在 WinForm 应用程序中
本文关键字:任务 应用程序 WinForm 布任务 API Web NET 客户端 程序 消息处理 ASP | 更新日期: 2023-09-27 17:56:41
我正在使用 MS ASP.NET Web API 客户端库中的HttpClient
和ProgressMessageHandler
。
我很高兴在控制台应用程序中毫无问题地修补了它,但现在在 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;
}
我无法想象这是性能最高的实现,坦率地说,它仍然感觉很笨拙。我能做得更好吗?
任何时候你试图混合同步和异步代码,你最终会得到一些混乱。(我有一篇博客文章解释了为什么这种死锁发生在 Windows 窗体上,而不是控制台应用程序上)。
最好的解决方案是 让它都async
.
(再次出于遗留原因),我实际上希望按钮单击处理程序阻止,直到异步返回。
考虑一些替代方案。是否可以禁用async
单击处理程序开头的按钮并在结束时重新启用它?这是一种相当常见的方法。
也许如果您描述为什么希望按钮单击处理程序阻止(在另一个问题中),我们可以建议替代解决方案。
我能设计的最简单的替代方案是[使用Task.Run]。我无法想象这是性能最高的实现,坦率地说,它仍然感觉很笨拙。
这是一个与其他解决方案一样好的解决方案。如果调用层次结构中的所有await
都使用 ConfigureAwait(false)
,则可以直接调用PostUsingClient
Result
。Result
的一个问题是它将任何异常包装在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 的默认保持活动行为的好处。