3

我正在使用MS HttpClientASP.NET Web API Client LibrariesProgressMessageHandler

我很高兴在控制台应用程序中对此进行了修补,没有问题,但现在在 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 建议的那样,让 async / await 模式从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;
}

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

4

2 回答 2

4

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

最好的解决办法是全力以赴async

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

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

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

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

它和其他任何解决方案一样好。如果您的所有s 在use的调用层次结构中,可以直接调用。一个问题是它会将任何异常包装在 中,因此您的错误处理也变得更加复杂。ResultawaitPostUsingClientConfigureAwait(false)ResultAggregateException

于 2013-01-30T14:30:38.213 回答
1

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

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

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

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

于 2013-01-30T15:08:29.757 回答