ASP.NET 配置等待

当 ASP.NET 处理请求时,将从线程池分配一个线程并创建一个请求上下文。请求上下文包含有关当前请求的信息,可通过静态 HttpContext.Current 属性访问该请求。然后将请求的请求上下文分配给处理请求的线程。

给定的请求上下文可能一次只能在一个线程上激活

当执行到达 await 时,处理请求的线程将返回到线程池,同时异步方法运行并且请求上下文可供另一个线程使用。

public async Task<ActionResult> Index()
{
    // Execution on the initially assigned thread
    var products = await dbContext.Products.ToListAsync();

    // Execution resumes on a "random" thread from the pool
    // Execution continues using the original request context.
    return View(products);
}

任务完成后,线程池会分配另一个线程以继续执行请求。然后将请求上下文分配给该线程。这可能是也可能不是原始主题。

闭塞

async 方法调用的结果等待**同步时,**可能会出现死锁。例如,以下代码将在调用 IndexSync() 时导致死锁:

public async Task<ActionResult> Index()
{
    // Execution on the initially assigned thread
    List<Product> products = await dbContext.Products.ToListAsync();

    // Execution resumes on a "random" thread from the pool
    return View(products);
}

public ActionResult IndexSync()
{
    Task<ActionResult> task = Index();

    // Block waiting for the result synchronously
    ActionResult result = Task.Result;

    return result;       
}

这是因为,默认情况下等待的任务,在这种情况下 db.Products.ToListAsync() 将捕获上下文(在 ASP.NET 的情况下为请求上下文)并在完成后尝试使用它。

当整个调用堆栈是异步的时候没有问题,因为一旦到达 await,原始线程就会释放,从而释放请求上下文。

当我们使用 Task.ResultTask.Wait()(或其他阻塞方法)同步阻塞时,原始线程仍处于活动状态并保留请求上下文。等待的方法仍然异步操作,一旦回调尝试运行,即一旦等待的任务返回,它就会尝试获取请求上下文。

因此,出现死锁是因为当具有请求上下文的阻塞线程正在等待异步操作完成时,异步操作正在尝试获取请求上下文以便完成。

ConfigureAwait

默认情况下,对等待任务的调用将捕获当前上下文,并在完成后尝试在上下文中继续执行。

通过使用 ConfigureAwait(false),可以防止这种情况,并且可以避免死锁。

public async Task<ActionResult> Index()
{
    // Execution on the initially assigned thread
    List<Product> products = await dbContext.Products.ToListAsync().ConfigureAwait(false);

    // Execution resumes on a "random" thread from the pool without the original request context
    return View(products);
}

public ActionResult IndexSync()
{
    Task<ActionResult> task = Index();

    // Block waiting for the result synchronously
    ActionResult result = Task.Result;

    return result;       
}

当需要阻塞异步代码时,这可以避免死锁,但是这会以丢失连续中的上下文(调用 await 之后的代码)为代价。

在 ASP.NET 中,这意味着如果调用 await someTask.ConfigureAwait(false); 后的代码尝试从上下文访问信息,例如 HttpContext.Current.User,则信息已丢失。在这种情况下,HttpContext.Current 为空。例如:

public async Task<ActionResult> Index()
{
    // Contains information about the user sending the request
    var user = System.Web.HttpContext.Current.User;

    using (var client = new HttpClient())
    {
        await client.GetAsync("http://google.com").ConfigureAwait(false);
    }

    // Null Reference Exception, Current is null
    var user2 = System.Web.HttpContext.Current.User;

    return View();
}

如果使用 ConfigureAwait(true)(相当于根本没有 ConfigureAwait),那么 useruser2 都填充了相同的数据。

因此,通常建议在不再使用上下文的库代码中使用 ConfigureAwait(false)