NET多線程和異步總結(二)

承接上文。web

線程池

線程池主要有兩個好處:編程

  1. 避免線程建立和銷燬的開銷。
  2. 自動縮放:能夠按需增減線程的數量。

總之,Windows系統自帶了線程池的功能,一般狀況下,你不可能有更好的實現。因此只需瞭解如何使用。安全

Windows的線程池有兩種,分別是非託管線程池和託管線程池(即.NET線程池)。下面分別來介紹。多線程

非託管線程池

  • 每一個進程都有一個線程池,線程池有個IOCP。
  • 其中的線程分爲IO線程和工做者線程(或非IO線程)。架構

    • 其中工做者線程監聽線程池的IOCP。
    • IO線程專門執行APC的異步完成例程。在空閒時一直是可喚醒狀態。
  • 調用Windows API QueueUserWorkItem會讓一個監聽在IOCP上的工做者線程醒來,並執行例程。
  • 調用BindIOCompletionCallback把一個文件句柄綁定到線程池的IOCP上。當此文件有關的IO操做完成時,一個工做者線程會被喚醒來執行後面的操做。
  • 調用QueueUserWorkItem並傳入WT_EXECUTEINPERSISTENTTHREAD標識時,會將一個APC回調放入IO線程的APC隊列中。

託管線程池

  • 每一個CLR都有一個線程池,線程池有個IOCP。(通常來講一個進程有一個CLR,也可能有多個)
  • 其中的線程分爲工做者線程和IO線程。app

    • 工做者線程從任務隊列中取得任務並執行(不經過IOCP)。
    • IO線程則監聽IOCP,在喚醒時執行任務的ContinueWith集合中的任務。
  • 能夠調用Task.Run()ThreadPool.QueueUserWorkItem()來添加任務到任務隊列中。
  • 使用ThreadPool.UnsafeQueueNativeOverlapped()能夠將任務添加到IO線程中。但不多使用。

可見,託管與非託管線程池的差距是巨大的。異步

.NET多線程及異步編程模型

  1. .NET 3.5及以前
    只有Thread, ThreadPool等接口。直接操做線程。
  2. .NET 4
    引入了Task
  3. .NET 4.5.1 (C# 5.0)
    引入了TAP(基於Task的異步模式),引入了 async/await

下面來着重分析Task編程模式。async

Task初級

Task最重要的兩個方法是Task.Run()new Task().Start()。使用這兩個方法:異步編程

  • 調用者傳遞一個委託給Task。
  • 線程池的一個線程會執行此委託。
  • 返回值將保存在Task.Result中。
  • 調用Task.Wait()Task.Result會阻塞地等待工做線程執行結束。

如下圖示演示了這個過程具體經歷了什麼。函數

clipboard.png

在任務完成時繼續執行其餘Task

調用Task.ContinueWith(continueAction, continueOptions)。其中,ContinuationOptions是一個枚舉,提供了更多選項。

clipboard.png

這個過程如何理解呢?請看下圖。

clipboard.png

Task裏面有什麼

如下是一些主要的屬性:

  • Id
  • State
  • Reference of parent task
  • Reference of Task scheduler
  • Delegate
  • AsyncState (to pass method’s objects)
  • ExecutionContext
  • CancellationToken
  • Collection of ContinueWithTask

async/await的機制

它其實是Task.ContinueWith()的語法糖。

爲此咱們來看一段程序:

private async void btnDoStuff_Click()
{
    lblStatus.Content = "Doing Stuff";

    await Task.Delay(4000);

    lblStatus.Content = "After await";
}

它至關於

private void btnDoStuff_Click()
{
    lblStatus.Content = "Doing Stuff";

    Task t = Task.Delay(4000);

    t.ContinueWith(task => lblStatus.Content = "After await");
}

可是ContinueWith裏面的是一個委託,最好寫成函數。另寫一個函數最簡單,不過C#使用另外一種「狀態機」技術把這個函數寫在了原來的方法內。也就是說,第一段代碼實際上會被編譯爲(並不精確):

private void btnDoStuff_Click(int step)
{
    switch (step)
    {
        case 0:
            lblStatus.Content = "Doing Stuff";
            Task t = Task.Delay(4000);
            t.ContinueWith(task => btnDoStuff_Click(task.Step));
            break;
        case 1:
            lblStatus.Content = "After await";
            break;
    }
}

方法被添加了一個參數step, 第一次調用方法時step爲0,此後每進入一次則加一。由此則用一個方法實現了兩端邏輯,這即是狀態機重入技術。C#的另外一個語法糖:用yield實現IEnumerable接口也是採用這種技術。

Execution Context 執行上下文

它在多線程的比如空氣:你能夠不知道它,但它很是重要。ExecutionContext是爲了解決線程本地存儲在多線程中沒法傳遞的問題:總得有一種機制可以傳遞全局信息。不然只能經過函數調用參數傳遞了。
當一個線程發起異步調用的時候,ExecutionContext會自動的在線程之間傳遞如下信息:

  • 線程安全設置
  • Host設置(與web服務有關)
  • Logical Call Context, 能夠在其中保存和傳遞對象。
  • 線程的Culture(從.NET 4.6之後)。

Synchronization Context 同步上下文

它是爲了描述異步調用返回時的行爲所建立的抽象。它有兩個基本接口方法:

  • Send 同步地等待任務執行完畢。
  • Post 把任務發出去就無論了。

那麼異步調用返回時的行爲是什麼意思?既然是抽象,那就會有具體的實現。後面咱們會看到幾種實現。

當開始異步調用時,C#會捕獲(capture)當前線程的同步上下文,並保存到Task中。在異步調用返回時,須要恢復(resume)同步上下文。此時就會調用同步上下文的Send或者Post

下面是幾種典型的同步上下文實現:

  1. UI同步上下文。因爲UI界面操做必須在UI線程中進行,所以這個上下文作的事情就是把須要恢復的工做Marshal起來交給UI線程。(可能有人會好奇如何交給UI線程去作。簡單來講, UI線程有個Windows消息循環,同步上下文將任務封裝在一個特定消息中,UI線程獲得這個消息後,就去執行其中的任務)。
  2. ASP.NET同步上下文。它有如下特色:

    • 不會切換線程,由於後臺線程沒什麼區別。
    • 會把線程的Principle和Culture傳遞過去。(由於ASP.NET依賴於此)
    • 在異步頁面中記錄還沒有完成的IO數量。
  3. 默認同步上下文。就是線程池的調度器,基本上沒有特別的操做。

最後,調用ConfigureAwait(false)時,就會跳過恢復同步上下文這一過程。因此,有時候必要(當不必傳遞任何信息時,使用它能夠提升效率),有時候又會出錯。例如,UI程序的異步調用原本沒問題,你加了這個語句,反而會形成修改界面的操做可能不在UI線程中執行,從而出錯。可是注意,不管如何,執行上下文都是會傳遞的。

結合以上,第一段程序的更精確的編譯後版本是這樣的:

private void btnDoStuff_Click(int step)
{
    switch (step)
    {
        case 0:
            lblStatus.Content = "Doing Stuff";
            Task t = Task.Delay(4000);
            t.ContinueWith(
                task => SynchronizationContext.Current.Post(
                 state => btnDoStuff_Click(task.Step), 
                 task)
            );
            break;
        case 1:
            lblStatus.Content = "After await";
            break;
    }
}

究竟是誰在執行異步調用?

這個問題曾經困擾我好久。若是我當前的線程調用一個異步調用後返回了,那究竟是誰在完成真正調用的工做呢?答案是一個(或幾個)共享的線程:線程池中的IO線程。

以下是一段代碼:

async void GetButton_OnClick(object o, EventArgs e)
{
    Task<Image> task = GetFaviconAsync(_url);

    Image image = await task;

    AddAFavicon(image);
}

async Task<Image> GetFaviconAsync(string url)
{
    var task = _webClient.DownloadDataTaskAsync(url);

    byte[] bytes = await task;

    return MakeImage(bytes);
}

線程的執行狀況以下圖:

clipboard.png

大部分的時間都在用戶線程中。只有調用到很是底層,IO完成以後,纔有IO線程被喚醒(見11),而後它調用Task的同步上下文的Post,將剩下的任務再交給用戶線程去執行。

下面是一個動態的解釋:

clipboard.png

ASP.NET應用的異步線程模型

關於IIS的架構和工做過程,有一些資料,這裏也不打算深究。提供兩張圖,以供理解。

clipboard.png

clipboard.png

相關文章
相關標籤/搜索