问题
I'm using HttpClient
and ProgressMessageHandler
from the MS ASP.NET Web API Client Libraries.
I've happily tinkered with this in a console application without issue, but now in a WinForm app, a "Post" task just gets plain old stuck on either .Wait()
or .Result
.
Below is a complete listing of my very simple test application. Button 1 works fine, Button 2 freezes every time on the call to postTask.Result
. Why?
Targetting 4.0 or 4.5 makes no difference. The same code in a console application has no issues.
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);
}
}
}
}
Update
OK, so it looks like this is my issue. For .NET 4.5, the obvious solution would be, as @StephenCleary suggests, to let the async / await pattern permeate from the PostAsJsonAsync
call all the way down into the button-click handlers. Something more like this:
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);
}
Now my problem is getting an equivalent solution in .NET 4.0 (for legacy reasons, of course). A close approximation would be to use a continuation in the button-click handler, trouble is (again for legacy reasons), I actually want the button-click handler to block until the async returns. I've found some creative solutions using the 4.0 compatible yield
operator, but they feel a little messy. Instead, the simplest alternative I can devise is:
private void button2_Click(object sender, EventArgs e)
{
var result = Task.Run(() => { return PostUsingClient(client); }).Result;
}
I can't imagine this is the most performant implementation, and it still feels frankly clumsy. Can I do better?
回答1:
Any time you try to mix synchronous and asynchronous code, you're going to end up with a bit of a mess. (I have a blog post explaining why this deadlock happens on Windows Forms but not Console apps).
The best solution is to make it all async
.
(again for legacy reasons), I actually want the button-click handler to block until the async returns.
Consider some alternatives. Would it be possible to disable the button at the beginning of the async
click handler and re-enable it at the end? This is a fairly common approach.
Perhaps if you describe why you want the button-click handler to block (in another question), we could suggest alternative solutions.
the simplest alternative I can devise is [using Task.Run.] I can't imagine this is the most performant implementation, and it still feels frankly clumsy.
It's as good a solution as any other. It is possible to directly call Result
if all your await
s in the call hierarchy of PostUsingClient
use ConfigureAwait(false)
. One problem with Result
is that it will wrap any exceptions in AggregateException
, so your error handling becomes more complex, too.
回答2:
I have used the following function to work around this issue (FOR RETROFITTING LEGACY ONLY).
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));
}
This works for me because it guarantees that when trying to run async stuff synchronously, it will always be done using the Threadpool TaskScheduler and never the TaskScheduler based on the SyncronizationContext. The problem you are running into is that because you are calling GetAsync on a continuation, it is inside a Task and by default all new tasks inherit the TaskScheduler of the creating task. So, you end up trying to run the GetAsync on the UI thread but then blocking the UI thread. When you are not running inside a Task, the default behaviour is to execute the GetAsync using the default task scheduler which is the thread pool scheduler. (At least that is my understanding so far)
On a different note, you probably don't want to be creating a new HttpClient on every request. Create one at the application scope, or at least at the form scope and re-use it. Every time you dispose the HttpClient it forcibly tries to close the underlying TCP connection, effectively eliminating the benefits of HTTP 1.1's default keep-alive behavior.
来源:https://stackoverflow.com/questions/14597232/asp-net-web-api-client-progressmessagehandler-post-task-stuck-in-winform-app