3

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?

Community
  • 1
  • 1
Snixtor
  • 4,239
  • 2
  • 31
  • 54
  • Why would you want the button-click handler to block? Blocking handlers are one of the biggest culprits for unresponsive UI. As far as I can tell, the 4.5 version using await is non-blocking. – Youssef Moussaoui Jan 30 '13 at 08:43
  • On re-examining it, you're right, the 4.5 version with `async void button2_Click` **is** non-blocking. The reason I want a blocking call is to retro-fit this to a large existing code-base. Existing button-clicks are blocking in this code-base, and downstream assumptions are made on that basis. Switching to (granted, obviously superior) non-blocking calls would turn this into a bigger project than preferred. – Snixtor Jan 30 '13 at 10:37

2 Answers2

4

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 awaits 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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Your blog post was instrumental in me finding a solution to my problem. I would be interesting in hearing your thoughts on my approach. – Darrel Miller Jan 30 '13 at 15:11
  • Glad to see you weigh in on the topic Stephen. Regarding the button-click needing to be synchronous, it's just one example. Worse than button-clicks is the proprietary automation / scripting language within it. Overhauling this scripting system to be async is **not** an option. I would happily use `ConfigureAwait(false)`, but I'm stuck in 4.0. My question has essentially become: "How can I call async methods from sync methods in 4.0?" – Snixtor Jan 30 '13 at 23:26
  • You should be able to use `ConfigureAwait` if you install [Microsoft.Bcl.Async](http://nuget.org/packages/Microsoft.Bcl.Async). – Stephen Cleary Jan 30 '13 at 23:30
  • The Bcl.Async library sounds promising. But a beta framework in production application is a little iffy. And as far as I can tell, this bug preventing `ConfigureAwait(false)` hasn't yet been fixed in a public release! - http://connect.microsoft.com/VisualStudio/feedback/details/767008/configureawait-false-still-continue-on-captured-context-when-using-async-targeting-pack – Snixtor Jan 31 '13 at 00:08
  • @Snixtor: Good catch on that bug. I've been looking forward to the next Bcl.Async release myself - there's some other bugs that affect my library. Hopefully soon... – Stephen Cleary Jan 31 '13 at 01:37
1

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.

Darrel Miller
  • 139,164
  • 32
  • 194
  • 243
  • A few corrections: an `async` continuation is not necessarily running within a `Task`. There is a `Task` that represents the `async` method as a whole, but continuations may be directly scheduled to a `SynchronizationContext` (not necessarily a `TaskScheduler`). So there's no `TaskScheduler` to inherit. Also, new tasks don't inherit schedulers by default - `await` has special logic to capture and resume in its context (`SynchronizationContext`, or `TaskScheduler` only if there isn't a `SynchronizationContext`). – Stephen Cleary Jan 30 '13 at 17:24
  • So `new TaskFactory(TaskScheduler.Default).StartNew` is the long form of `Task.Factory.StartNew`, which in this particular case (with a synchronous lambda) is equivalent to `Task.Run`. Also, you're not gaining anything by blocking a thread pool thread. – Stephen Cleary Jan 30 '13 at 17:25
  • 1
    I have an [async/await intro](http://nitoprograms.blogspot.com/2012/02/async-and-await.html) on my blog that explains all about the context capture and resume. – Stephen Cleary Jan 30 '13 at 17:25
  • @StephenCleary Thanks for the feedback. I'm still living in 4.0 worlds, so I'm not so familiar with how the asynchronous/await keywords use the tpl. It does appear that TaskFactory behaves differently depending on whether you are in a task or not. – Darrel Miller Jan 30 '13 at 17:35
  • @StephenCleary If I don't pass TaskScheduler.Default to the task factory then .Result will block if GetAsync is called from a continuation that is run using UITaskScheduler. – Darrel Miller Jan 30 '13 at 17:42
  • 1
    `new TaskFactory` will use the current `TaskScheduler`. But `Task.Factory` (and `Task.Run`) use the default `TaskScheduler`. – Stephen Cleary Jan 30 '13 at 17:59
  • @StephenCleary Ok, good to know. I was able to simplify my helper using Task.Run and it worked just fine. – Darrel Miller Jan 30 '13 at 18:06
  • @DarrelMiller Your *legacy only* solution looks a heck of a lot like my above `Task.Run` implementation. While `Task.Run` isn't an option in 4.0, its functional equivalent `Task.Factory.StartNew` **is**, so it sounds like that's *good enough* solution to the scenario. I'll have to re-read some of the above comments and Stephens blogs to get a better understanding of what's going on with `TaskScheduler` and `SynchronizationContext`, but I think all the information I need is easily to hand now. – Snixtor Jan 30 '13 at 23:33
  • @DarrelMiller Regarding keeping `HttpClient` open, i.e. running it as a singleton, I did come to learn about that recently too. Thanks for pointing out that potential problem as well as tackling my primary question. – Snixtor Jan 30 '13 at 23:36
  • @Snixtor When I originally read your post I didn't know Task.Run was equivalent. I also didn't know Task.Run was a 4.5 feature only. It's been a good learning day :-) – Darrel Miller Jan 31 '13 at 00:26
  • @Snixtor If you have a pluralsight subscription Ian Griffiths does a great course on the TPL. That cleared up lots of things regarding TaskSchedulers for me. – Darrel Miller Jan 31 '13 at 00:27
  • @DarrelMiller Definitely a lot to learn. I was still coming to terms with TPL when async / await got lumped on top of it. I don't have a Pluralsight subscription, but will be scheduling some time to watch "The zen of async" by Stephen Toub - http://channel9.msdn.com/Events/Build/BUILD2011/TOOL-829T – Snixtor Jan 31 '13 at 00:39
  • @StephenCleary new TaskFactory(TaskScheduler.Default).StartNew and Task.Factory.StartNew are only equivalent if there is not a current task with scheduler different that Default. – Darrel Miller Apr 19 '13 at 16:16
  • @DarrelMiller: I stand corrected; `Task.Factory` will inherit the task scheduler from the currently-running task. [FYI, Toub has an excellent writeup here.](http://blogs.msdn.com/b/pfxteam/archive/2012/09/22/new-taskcreationoptions-and-taskcontinuationoptions-in-net-4-5.aspx) – Stephen Cleary Apr 19 '13 at 16:30