1. 程式人生 > >ASP.NET sync over async(異步中同步,什麽鬼?)

ASP.NET sync over async(異步中同步,什麽鬼?)

bsp 只有一個 send 寫法 efault get c-c arch for

轉自:http://www.cnblogs.com/xishuai/p/asp-net-sync-over-async.html

async/await 是我們在 ASP.NET 應用程序中,寫異步代碼最常用的兩個關鍵字,使用它倆,我們不需要考慮太多背後的東西,比如異步的原理等等,如果你的 ASP.NET 應用程序是異步到底的,包含數據庫訪問異步、網絡訪問異步、服務調用異步等等,那麽恭喜你,你的應用程序是沒問題的,但有一種情況是,你的應用程序代碼比較老,是同步的,但現在你需要調用異步代碼,這該怎麽辦呢?有人可能會說,很簡單啊,不是有個 .Result 嗎?但事實真的就這麽簡單嗎?我們來探究下。

首先,放出幾篇經典文章:

  • async & await 的前世今生
  • 異步編程 In .NET(中文資料中,寫異步最棒的兩篇文章
  • HttpClient.GetAsync(…) never returns when using await/async
  • Don‘t Block on Async Code(下面測試中的第三種情況
  • Should I expose synchronous wrappers for asynchronous methods?(sync over async 透徹

上面文章的內容,我們後面會說。光看不練假把式,所以,如果真正要體會 sync over async,我們還需要自己動手進行測試:

  • 1. 異步調用使用 .Result,同步調用使用 .Result
  • 2. 異步調用使用 await,同步調用使用 Task.Run
  • 3. 異步調用使用 await,同步調用使用 .Result
  • 4. 異步調用使用 Task.Run,同步調用使用 .Result
  • 5. 異步調用使用 await .ConfigureAwait(true),同步調用使用 .Result
  • 6. 異步調用使用 await .ConfigureAwait(false),同步調用使用 .Result
  • 7. 異步調用使用 await,異步調用使用 await
  • 8. 測試總結

先說明一下,在測試代碼中,異步調用使用的是 HttpClient.GetAsync 方法,並且測試請求執行兩次,關於具體的分析,後面再進行說明。

1. 異步調用使用 .Result,同步調用使用 .Result

測試代碼:

[Route("")]
[HttpGet]
public string Index()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
    var result = Test();
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
    return result;
}

public static string Test()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
    using (var client = new HttpClient())
    {
        var response = client.GetAsync(url).Result;
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
        return response.Content.ReadAsStringAsync().Result;
    }
}

輸出結果:

Thread.CurrentThread.ManagedThreadId1:13
Thread.CurrentThread.ManagedThreadId2:13
Thread.CurrentThread.ManagedThreadId3:13
Thread.CurrentThread.ManagedThreadId4:13
Thread.CurrentThread.ManagedThreadId1:6
Thread.CurrentThread.ManagedThreadId2:6
Thread.CurrentThread.ManagedThreadId3:6
Thread.CurrentThread.ManagedThreadId4:6

簡單總結:同步代碼中調用異步,上面的測試代碼應該是我們最常寫的,為什麽沒有出現線程阻塞,頁面卡死的情況呢?而且代碼中調用了 GetAsync,為什麽請求線程只有一個?後面再說,我們接著測試。

2. 異步調用使用 await,同步調用使用 Task.Run

測試代碼:

[Route("")]
[HttpGet]
public string Index()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
    var result = Task.Run(() => Test2()).Result;
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
    return result;
}

public static async Task<string> Test2()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
    using (var client = new HttpClient())
    {
        var response = await client.GetAsync(url);
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
        return await response.Content.ReadAsStringAsync();
    }
}

輸出結果:

Thread.CurrentThread.ManagedThreadId1:6
Thread.CurrentThread.ManagedThreadId2:7
Thread.CurrentThread.ManagedThreadId3:11
Thread.CurrentThread.ManagedThreadId4:6
Thread.CurrentThread.ManagedThreadId1:6
Thread.CurrentThread.ManagedThreadId2:7
Thread.CurrentThread.ManagedThreadId3:12
Thread.CurrentThread.ManagedThreadId4:6

簡單總結:根據上面的輸出結果,我們發現,在一個請求過程中,總共會出現三個線程,一個是開始的請求線程,接著是 Task.Run 創建的一個線程,然後是異步方法中 await 等待的執行線程,需要註意的是,ManagedThreadId1 和 ManagedThreadId4 始終是一樣的。

3. 異步調用使用 await,同步調用使用 .Result

測試代碼:

[Route("")]
[HttpGet]
public string Index()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
    var result = Test3().Result;
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
    return result;
}

public static async Task<string> Test3()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
    using (var client = new HttpClient())
    {
        var response = await client.GetAsync(url);
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
        return await response.Content.ReadAsStringAsync();
    }
}

輸出結果:

Thread.CurrentThread.ManagedThreadId1:5
Thread.CurrentThread.ManagedThreadId2:5

簡單總結:首先,頁面是卡死狀態,ManagedThreadId3 並沒有輸出,也就是執行到 await client.GetAsync 的時候,線程就阻塞了。

4. 異步調用使用 Task.Run,同步調用使用 .Result

測試代碼:

[Route("")]
[HttpGet]
public string Index()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
    var result = Test4().Result;
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
    return result;
}

public static async Task<string> Test4()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
    return await Task.Run(() =>
    {
        Thread.Sleep(1000);
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
        return "xishuai";
    });
}

輸出結果:

Thread.CurrentThread.ManagedThreadId1:6
Thread.CurrentThread.ManagedThreadId2:6
Thread.CurrentThread.ManagedThreadId3:7

簡單總結:和第三種情況一樣,頁面也是卡死狀態,但不同的是,ManagedThreadId3 是輸出的,測試它的主要目的是和第三種情況形成對比,以便了解 HttpClient.GetAsync 中到底是什麽鬼?

5. 異步調用使用 await .ConfigureAwait(true),同步調用使用 .Result

測試代碼:

[Route("")]
[HttpGet]
public string Index()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
    var result = Test5().Result;
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
    return result;
}

public static async Task<string> Test5()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
    using (var client = new HttpClient())
    {
        var task = client.GetAsync(url);
        var response = await task.ConfigureAwait(true);
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
        return await response.Content.ReadAsStringAsync();
    }
}

輸出結果:

Thread.CurrentThread.ManagedThreadId1:6
Thread.CurrentThread.ManagedThreadId2:6

簡單總結:和上面兩種情況一樣,頁面也是卡死狀態,它的效果和第三種完全一樣,ManagedThreadId3 都沒有輸出的。

6. 異步調用使用 await .ConfigureAwait(false),同步調用使用 .Result

測試代碼:

[Route("")]
[HttpGet]
public string Index()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
    var result = Test6().Result;
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
    return result;
}

public static async Task<string> Test6()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
    using (var client = new HttpClient())
    {
        var task = client.GetAsync(url);
        var response = await task.ConfigureAwait(false);
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
        return await response.Content.ReadAsStringAsync();
    }
}

輸出結果:

Thread.CurrentThread.ManagedThreadId1:6
Thread.CurrentThread.ManagedThreadId2:6
Thread.CurrentThread.ManagedThreadId3:10
Thread.CurrentThread.ManagedThreadId4:6
Thread.CurrentThread.ManagedThreadId1:8
Thread.CurrentThread.ManagedThreadId2:8
Thread.CurrentThread.ManagedThreadId3:11
Thread.CurrentThread.ManagedThreadId4:8

簡單總結:和第五種情況形成對比,僅僅只是把 ConfigureAwait 參數設置為 false,結果卻完全不同。

7. 異步調用使用 await,異步調用使用 await

測試代碼:

[Route("")]
[HttpGet]
public async Task<string> Index()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
    var result = await Test7();
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
    return result;
}

public static async Task<string> Test7()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
    using (var client = new HttpClient())
    {
        var response = await client.GetAsync(url);
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
        return await response.Content.ReadAsStringAsync();
    }
}

輸出結果:

Thread.CurrentThread.ManagedThreadId1:6
Thread.CurrentThread.ManagedThreadId2:6
Thread.CurrentThread.ManagedThreadId3:12
Thread.CurrentThread.ManagedThreadId4:12
Thread.CurrentThread.ManagedThreadId1:7
Thread.CurrentThread.ManagedThreadId2:7
Thread.CurrentThread.ManagedThreadId3:8
Thread.CurrentThread.ManagedThreadId4:8

簡單總結:註意這是異步的寫法,調用和被調用方法都是異步的,從輸出的結果中,我們就會發現,這種情況和上面的六種情況,有一個最明顯的區別就是,請求線程和結束線程不是同一個,說明什麽呢?線程是異步等待的。

8. 測試總結

先梳理一下測試結果:

  1. 異步調用使用 .Result,同步調用使用 .Result:通過,始終一個線程。
  2. 異步調用使用 await,同步調用使用 Task.Run:通過,三個線程,請求開始和結束為相同線程。
  3. 異步調用使用 await,同步調用使用 .Result:卡死,線程阻塞。
  4. 異步調用使用 Task.Run,同步調用使用 .Result:卡死,線程阻塞。
  5. 異步調用使用 await .ConfigureAwait(true),同步調用使用 .Result:卡死,線程阻塞。
  6. 異步調用使用 await .ConfigureAwait(false),同步調用使用 .Result:通過,兩個線程,await 執行為單獨一個線程。
  7. 異步調用使用 await,異步調用使用 await:通過,兩個線程,請求開始和結束為不同線程。

上面這麽多的測試情況,看起來可能有些暈,我們先從最簡單的第二種情況開始分析下,首先,頁面是同步方法,請求線程可以看作是一個主線程 1,然後通過 Task.Run 創建線程 2,讓它去執行 Test2 方法,需要註意的是,這時候主線程 1 並不會往下執行(從輸出結果可以看出),它會等待線程 2 執行,主要是等待線程 2 執行返回結果,在 Test2 方法中,一切是異步方法,await client.GetAsync 會創建又一個線程 3 去執行,並且線程 2 等待它返回結果,然後最終回到線程 1 上,在整個過程中,雖然有三個線程,但這三個線程並不是同時工作的,而是一個執行之後等待另一個執行的結果,所以整個執行過程還是同步的。

第三種和第二種情況的不同就是,異步調用由 Task.Run 改成了 .Result,然後就造成了頁面卡死,在 Don‘t Block on Async Code 這篇文章中,就是詳細說明的這種情況,為什麽會卡死呢?其實你從同樣卡死的第四種情況和第五種情況中,可以發現一些線索,ConfigureAwait 的說明是:試圖繼續回奪取的原始上下文,則為 true;否則為 false。什麽意思呢?就是它可以變身為請求線程,最能體現出這一點的是,如果設置為 true,那麽在這個線程中,就可以訪問 HttpContext.Current,那為什麽在同步調用中,設置為 true 就造成頁面卡死呢?我們分析一下,頁面是同步方法,請求線程可以看作是一個主線程 1,然後調用 Test3 異步方法,這時候主線程 1,會在這裏等待異步的執行結果,在 Test3 方法中創建一個線程 2,因為把 ConfigureAwait 設置為了 true,那麽線程 2 就想把自己變身成為請求線程(謀權篡位),也就是線程 1,但是人家線程 1 現在正在門口等它呢?線程 2 卻想占有線程 1 的地位,很顯然,這是不成功的,那什麽情況下可以謀權篡位成功呢?就是線程 1 不在,也就是線程 1 回到線程池中了,這就是異步等待的效果,也是它的威力。

針對第三種情況,簡單畫了一個示意圖:

技術分享圖片

在第五種情況中,因為把 ConfigureAwait 設置為 false,線程 2 不想謀權篡位了,它只想老老實實的做事,把執行結果返回給請求線程 1,那麽整個請求執行過程就是順利的。

同步調用異步測試中,還剩一個第一種情況,它和其他情況不同的是,沒有異步方法,只是使用的是 .Result,那為什麽它是通過的?並且線程始終是一個呢?首先,頁面請求開始,創建一個請求線程 1,因為 Test 方法並不是異步方法,所以還是線程 1 去執行它,執行到了 client.GetAsync 這一步,因為沒有使用 await,所以並不會創建一個線程去執行它,並且最終的是,雖然 GetAsync 是異步方法,但再其實現代碼中,設置了 ConfigureAwait(false):

async Task<HttpResponseMessage> SendAsyncWorker(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
    using (var lcts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken))
    {
        lcts.CancelAfter(timeout);

        var task = base.SendAsync(request, lcts.Token);
        if (task == null)
            throw new InvalidOperationException("Handler failed to return a value");

        var response = await task.ConfigureAwait(false);//重點
        if (response == null)
            throw new InvalidOperationException("Handler failed to return a response");

        //
        // Read the content when default HttpCompletionOption.ResponseContentRead is set
        //
        if (response.Content != null && (completionOption & HttpCompletionOption.ResponseHeadersRead) == 0)
        {
            await response.Content.LoadIntoBufferAsync(MaxResponseContentBufferSize).ConfigureAwait(false);
        }

        return response;
    }
}

所以,整個過程應該是這樣的,在測試代碼中始終是一個請求線程在執行,並且在 client.GetAsync 的執行中,會創建另外一個線程 2 去執行,然後線程 1 等待線程 2 的執行結果,因為 GetAsync 的實現並不在測試代碼中,所以表現出來就是一個線程在執行,雖然是異步方法,但它和同步方法一樣,為什麽?因為線程始終在等待另一個線程的執行結果,也就是說,在某一時刻,始終是一個線程在執行,其余線程都在等待。

sync over async(異步中同步)是否可行?通過上面的測試結果可以得出是可行的,但要註意一些寫法問題:

  • 異步調用使用 .Result,而不能出現 await。
  • 不能出現 ConfigureAwait(true)。
  • 可以使用 Task.Run,但僅限於不返回結果的執行線程。

當然最好的方式是異步到底

ASP.NET sync over async(異步中同步,什麽鬼?)