.NET - Deadlock in await

  • 如何避免死锁

下面的例子,要展示的是await死锁的情形,在Console模式下正常,在ASP.NET下死锁

  • 公共Model

在Console和ASP.NET中共用

using System;
using System.Threading.Tasks;
 
namespace AspNetAwaitDemo.Models
{
    public class AwaitDemo
    {
        static int _nextInt;
 
        static AwaitDemo()
        {
            _nextInt = new Random().Next() % 10000;
        }
 
        // 同步方法,执行时间5秒

        private static int DoWork()
        {
            System.Threading.Thread.Sleep(5000);
            return _nextInt++;
        }
 
        // 异步方法,调用上述同步方法 DoWork

        public static async Task<int> GetNextIntAsync()
        {
            var task = Task.Run(() => {
                var ret = DoWork();
                return ret;
            });
            var val = await task;
            return val;
        }
 
        // 同步方法,调用上述异步方法 GetNextIntAsync,并设置超时最多等待15秒

        public static string GetNextString()
        {
            var task = GetNextIntAsync();
            if (task.Wait(new TimeSpan(0, 0, 15)) == false) {
                throw new TimeoutException();
            }
            return task.Result.ToString();
        }
    }
}
  • Console App
using AspNetAwaitDemo.Models;
using System;
 
namespace DemoApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var s = AwaitDemo.GetNextString();
            Console.WriteLine(s);
        }
    }
}
  • Asp.Net App
using AspNetAwaitDemo.Models;
using System.Web.Mvc;
 
namespace AspNetAwaitDemo.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var s = AwaitDemo.GetNextString();
            return View();
        }
    }
}
  • 解决方法

把上述Model中的异步方法稍作修改,则可以避免死锁,详细可以参考:这里

public static async Task<int> GetNextIntAsync()
{
    var task = Task.Run(() => {
        var ret = DoWork();
        return ret;
    });
    var val = await task.ConfigureAwait(false);
    return val;
}

经过测试,ASP.NET中,对调用栈中的每个await都要ConfigureAwait才能避免死锁。

  • ConfigureAwait
public System.Runtime.CompilerServices.ConfiguredTaskAwaitable<TResult> ConfigureAwait (bool continueOnCapturedContext);
 
/*
Parameters
  - continueOnCapturedContext: Boolean
    true to attempt to marshal the continuation back to the original context captured; otherwise, false.
*/
  • Asp.Net Core

在ASP.NET CORE 2.0里面,这个问题已经不在存在(已在ASP.NET CORE 2.0 MVC 项目中测试验证),用户无需调用ConfigureAwait(false),详情请见如下描述

Since there is no context anymore, there’s no need for ConfigureAwait(false). Any code that knows it’s running under ASP.NET Core does not need to explicitly avoid its context. In fact, the ASP.NET Core team themselves have dropped the use of ConfigureAwait(false). However, I still recommend that you use it in your core libraries – anything that may be reused in other applications. If you have code in a library that may also run in a UI app, or legacy ASP.NET app, or anywhere else there may be a context, then you should still use ConfigureAwait(false) in that library.