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

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

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

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

上面文章的內容,咱們後面會說。光看不練假把式,因此,若是真正要體會 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;
    }
}

 

輸出結果:app

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

測試代碼:async

[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,但僅限於不返回結果的執行線程。

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

相關文章
相關標籤/搜索