Freigeben über


Fun with ConfigureAwait, and deadlocks in ASP.NET

Coworker and I investigated a cool issue with deadlocks we found some time ago when porting some async code from one service to another. The difference was that one was written with async/await down to the controller, and the other follows legacy Task.Run model. When trying to code like this we encountered a deadlock:

     public class HomeController : Controller
    {
        public static async Task GetJsonAsync(Uri uri)
        {
            using (var client = new HttpClient())
            {
                var jsonString = await client.GetStringAsync(uri); 
                return jsonString;
            }
        }

        public ActionResult Index()
        {
            var jsonTask = GetJsonAsync(new Uri("https://example.com"));

            ViewBag.Title = jsonTask.Result.ToString();       // deadlock here!!

            return View();
        }
    }

We were trying to figure it out, and it worked with a different approach:

     public class HomeController : Controller
    {
        public static async Task GetJsonAsync(Uri uri)
        {
            using (var client = new HttpClient())
            {
                var jsonString = await client.GetStringAsync(uri);
                return jsonString;
            }
        }

        public ActionResult Index()
        {
            var jsonTask = Task.Run(() => GetJsonAsync(new Uri("https://example.com")));

            ViewBag.Title = jsonTask.Result.ToString();       // works fine!!

            return View();
        }
    }

I remembered it could have to be something with synchronization context in ASP.NET, but didn’t quite remember what exactly. I googled up a bit, and found this explanation:

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

So, according to it, ASP.NET has a specialized sync context, that only one thread can have. And if you don’t use .ConfigureAwait(false), it tries to restore old context, which belongs to the main thread that is blocked by .Result, hence deadlock. In the second example, the func is run on a different thread, and doesn’t need ASP.NET context, which is also blocked, but it’s OK now.

With ConfigureAwait it also works, without a thread:

         public static async Task GetJsonAsync(Uri uri)
        {
            using (var client = new HttpClient())
            {
                var jsonString = await client.GetStringAsync(uri).ConfigureAwait(false);
                return jsonString;
            }
        }

        public ActionResult Index()
        {
            var jsonTask = GetJsonAsync(new Uri("https://example.com"));

            ViewBag.Title = jsonTask.Result.ToString();       // works fine!!

            return View();
        }

What on earth exactly is a sync context, and why ASP.NET behaves this way? This looks like a good intro:

https://msdn.microsoft.com/en-us/magazine/gg598924.aspx

Comments

  • Anonymous
    December 15, 2017
    I wonder how do you return a string with async Task in GetJsonAsync???
    • Anonymous
      December 28, 2017
      It's because it's marked async. In reality it returns a Task. You can see it in decompiler.