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)