I don't know if I am doing something wrong or I found a bug in the Async library, but I have seen an issue when running some async code after I came back to the Synchronized context with continueWith().
UPDATE: The code now runs
using System;
using System.ComponentModel;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
internal static class Program
{
[STAThread]
private static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
MainFrameController controller = new MainFrameController(this);
//First async call without continueWith
controller.DoWork();
//Second async call with continueWith
controller.DoAsyncWork();
}
public void Callback(Task<HttpResponseMessage> task)
{
Console.Write(task.Result); //IT WORKS
MainFrameController controller =
new MainFrameController(this);
//third async call
controller.DoWork(); //IT WILL DEADLOCK, since ConfigureAwait(false) in HttpClient DOESN'T change context
}
}
internal class MainFrameController
{
private readonly Form1 form;
public MainFrameController(Form1 form)
{
this.form = form;
}
public void DoAsyncWork()
{
Task<HttpResponseMessage> task = Task<HttpResponseMessage>.Factory.StartNew(() => DoWork());
CallbackWithAsyncResult(task);
}
private void CallbackWithAsyncResult(Task<HttpResponseMessage> asyncPrerequisiteCheck)
{
asyncPrerequisiteCheck.ContinueWith(task =>
form.Callback(task),
TaskScheduler.FromCurrentSynchronizationContext());
}
public HttpResponseMessage DoWork()
{
MyHttpClient myClient = new MyHttpClient();
return myClient.RunAsyncGet().Result;
}
}
internal class MyHttpClient
{
public async Task<HttpResponseMessage> RunAsyncGet()
{
HttpClient client = new HttpClient();
return await client.GetAsync("https://www.google.no").ConfigureAwait(false);
}
}
partial class Form1
{
private IContainer components;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Text = "Form1";
}
#endregion
}
}
The main problem in your code is due to StartNew and ContinueWith. ContinueWith is dangerous for the same reasons that StartNew is dangerous, as I describe on my blog.
In summary: StartNew and ContinueWith should only be used if you're doing dynamic task-based parallelism (which this code is not).
The actual problem is that HttpClient.GetAsync doesn't use (the equivalent of) ConfigureAwait(false); it's using ContinueWith with its the default scheduler argument (which is TaskScheduler.Current, not TaskScheduler.Default).
To explain in more detail...
The default scheduler for StartNew and ContinueWith is not TaskScheduler.Default (the thread pool); it's TaskScheduler.Current (the current task scheduler). So, in your code, DoAsyncWork as it currently is does not always execute DoWork on the thread pool.
The first time DoAsyncWork is called, it will be called on the UI thread but without a current TaskScheduler. In this case, TaskScheduler.Current is the same as TaskScheduler.Default, and DoWork is called on the thread pool.
Then, CallbackWithAsyncResult invokes Form1.Callback with a TaskScheduler that runs it on the UI thread. So, when Form1.Callback calls DoAsyncWork, it is called on the UI thread with a current TaskScheduler (the UI task scheduler). In this case, TaskScheduler.Current is the UI task scheduler, and DoAsyncWork ends up calling DoWork on the UI thread.
For this reason, you should always specify a TaskScheduler when calling StartNew or ContinueWith.
So, this is a problem. But it's not actually causing the deadlock you're seeing, because ConfigureAwait(false) should allow this code to just block the UI instead of deadlocking.
It's deadlocking because Microsoft made the same mistake. Check out line 198 here: GetContentAsync (which is called by GetAsync) uses ContinueWith without specifying a scheduler. So, it's picking up the TaskScheduler.Current from your code, and will not ever complete its task until it can run on that scheduler (i.e., the UI thread), causing the classic deadlock.
There's nothing you can do to fix the HttpClient.GetAsync bug (obviously). You'll just have to work around it, and the easiest way to do that is to avoid having a TaskScheduler.Current. Ever, if you can.
Here's some general guidelines for asynchronous code:
StartNew. Use Task.Run instead.ContinueWith. Use await instead.Result. Use await instead.If we just do minimal changes (replacing StartNew with Run and ContinueWith with await), then DoAsyncWork always executes DoWork on the thread pool, and the deadlock is avoided (since await uses the SynchronizationContext directly and not a TaskScheduler):
public void DoAsyncWork()
{
Task<HttpResponseMessage> task = Task.Run(() => DoWork());
CallbackWithAsyncResult(task);
}
private async void CallbackWithAsyncResult(Task<HttpResponseMessage> asyncPrerequisiteCheck)
{
try
{
await asyncPrerequisiteCheck;
}
finally
{
form.Callback(asyncPrerequisiteCheck);
}
}
However, it's always questionable to have a callback scenario with Task-based asynchrony, because Tasks themselves have the power of callbacks within them. It looks like you're trying to do a sort of asynchronous initialization; I have a blog post on asynchronous construction that shows a few possible approaches.
Even something really basic like this would be a better design than callbacks (again, IMO), even though it uses async void for initialization:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
MainFrameController controller = new MainFrameController();
controller.DoWork();
Callback(controller.DoAsyncWork());
}
private async void Callback(Task<HttpResponseMessage> task)
{
await task;
Console.Write(task.Result);
MainFrameController controller = new MainFrameController();
controller.DoWork();
}
}
internal class MainFrameController
{
public Task<HttpResponseMessage> DoAsyncWork()
{
return Task.Run(() => DoWork());
}
public HttpResponseMessage DoWork()
{
MyHttpClient myClient = new MyHttpClient();
var task = myClient.RunAsyncGet();
return task.Result;
}
}
Of course, there's other design problems here: namely, DoWork is blocking on a naturally-asynchronous operation, and DoAsyncWork is blocking a thread pool thread on a naturally-asynchronous operation. So, when Form1 calls DoAsyncWork, it's awaiting a thread pool task that's blocked on an asynchronous operation. Async-over-sync-over-async, that is. You may also benefit from my blog series on Task.Run etiquette.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With