1. 程式人生 > >C#異步編程(一)線程及異步編程基礎

C#異步編程(一)線程及異步編程基礎

public 訪問 疊加 ade 上下 closed clear stat sum

  最近試著做了幾個.NET CORE的demo,看了些源碼,感覺異步編程在Core裏面已經成為主流,而對這塊我還沒有一個系統的總結,所以就出現了這篇文字,接下來幾篇文章,我會總結下異步編程的思路,主要參考clr via c#及以前看過的優秀博文。第一篇文字,我們一起來就打牢基礎,把線程基礎知識梳理一遍。

  本文完全原創,如果轉載請註明原文作者及鏈接。

一、線程基礎

每個線程都有以下要素

線程內核對象(thread kernael object)

os為系統中創建的每個線程都分配並初始化這種數據結構,包含一組對線程進行描述的屬性,還包含所謂的線程山下文(thread context)。上下文是包含cpu寄存器集合的內存塊。對於x 86,x64和arm cpu架構,線程上下文分別使用700,1240和350字節的內存。

線程環境塊(thread environment block,TEB)

TEB是在用戶模式(應用程序代碼能快速訪問的地址空間)中分配和初始化的內存塊。Teb耗用1個內存頁。TEB包含線程的異常處理鏈首。線程進入的每個try塊都在鏈首插入一個節點,線程退出try塊時從鏈中刪除該節點。此外,還包含線程的“線程本地存儲”數據,以及由GDI(graphics device interface,圖形設備接口)和opengl圖形使用的一些數據結構

用戶模式棧(user-mode stack)

用戶模式棧存儲傳給方法的局部變量和實參。他還包含一個地址:指出當前的方法返回時,線程應該從什麽地方接著執行。windows默認給每個線程的用戶模式棧分配1mb內存。更具體地說,winows只是保留1mb地址空間,在線程實際需要時才會調撥物理內存。

內核模式棧(kernel-mode stack)

所謂的內核模式,主要是核心操作系統組件在內核模式下運行,很多驅動程序在內核模式下運行,內核模式效率更高,如果內核模式驅動程序損壞,則整個操作系統會損壞。

應用程序代碼向操作系統中的內核模式函數傳遞實參時,還會使用內核模式棧。出於對按全額考慮,針對從用戶模式的代碼傳遞給內核的任何實參,windows都會把他們從線程的用戶模式棧復制到線程的內核模式棧。一經賦值,內核就可以驗證實參的值。由於應用程序代碼不能訪問內核模式棧,所以應用程序無法更改驗證後的實參值。32位windows 內核棧大小12kb,64位windows是24kb。

DLL線程連接(attach)和線程分離(detach)通知

windows的一個策略是,任何時候在進程中創建線程,都會調用進程中加載的所有非托管dll的dllmain方法,並向該方法傳遞dll_thread_attach標誌。類似地,任何時候線程終止,都會調用進程中的所有非托管dll的dllmain方法,並向方法傳遞dll_thread_detach標誌。有的dll需要獲取這些同誌,才能為進程中創建/銷毀的每個線程執行特殊的初始化或(資源)清理操作。

  1.1 windows系統線程切換

  cpu的單個核心同一時間只能進行一個線程的執行(不考慮intel超線程技術),在執行的線程可以運行一個“時間片”(quantum,也叫“量程”)。時間片時間片到期,windows進行線程切換所執行的操作:

1、 將cpu寄存器的值保存到當前正在運行的線程的內核對象內部的一個上下文結構中。

2、 從現有線程集合中選出一個線程進行執行。(如果該線程由另一個進程擁有,windows在開始執行任何代碼之前,還必須切換虛擬地址空間到對應的進程)

3、 將所選執行線程上下文結構中的值加載到cpu·寄存器中

  雖然我們看到的是以上的三步,但是實際執行中,線程切換對性能的影響可能比以上三步的消耗更多。比如,cpu現在要執行一個不同的線程,而之前的線程的代碼和數據還在cpu的告訴緩存(cache)中,這使cpu不必經常訪問ram。而一旦進行上下文切換到新的線程,這個新的線程很大概率執行不同的代碼,訪問不同的數據,這些代碼和數據並不在告訴緩存中,因此,cpu必須訪問ram來填充他的高速緩存。

  1.2 線程調度的優先級

  windows之所以被稱為搶占式多線程(preemptive multithreaded)操作系統,是因為線程可以在任何時間停止(被搶占)並調度另一個線程。程序員在這方面有一定的控制權,雖然不多。記住一點,你不能保證自己的線程一直在運行,你阻止不了其他線程的運行。

在windows中,每個線程都分配了從0(最低)到31(最高的優先級),系統決定為cpu分配哪個線程時,會以一種輪流的方式調度他。

線程的優先級是進程優先級和線程本身優先級疊加後計算出來的,如下圖。

技術分享圖片

二、異步編程

  2.1 clr線程池

  創建和銷毀線程是一個昂貴的操作,為了改善這個情況,clr包含了代碼來管理自己的線程池(thread pool)。每個clr一個線程池:這個線程池由clr控制的所有AppDomain共享。

線程池具體維護多少的線程根據程序的請求頻次有關,這個clr有內部的算法,我們這裏不進行深入討論。

  2.2 異步操作的取消

異步操作的取消可以使用CancellationTokenSource類

技術分享圖片

簡單的代碼實例如下

internal static class CancellationDemo
{
    public static void Go()
    {
        CancellationTokenSource cts = new CancellationTokenSource();

        ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000));

        Console.WriteLine("press <enter> to cancel the operation");

        Console.ReadLine();
        cts.Cancel();//如果count方法已經返回,cancel沒有任何效果

        Console.ReadLine();
    }
    private static void Count(CancellationToken token, int countTo)
    {
        for (int count = 0; count < countTo; count++)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("Count is cancelled");
                break;
            }
            Console.WriteLine(count);
            Thread.Sleep(200);
        }
        Console.WriteLine("Count is done");
    }
}

  2.3 Task

  QueueUserWorkItem沒有內建機制讓你知道操作在什麽時候完成,也沒有機制在操作完成時獲取返回值。為了克服這些限制(並解決其他一些問題),microsoft引入了任務的概念。

調用方式如下:

ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000));

Task.Run(() => Count(cts.Token, 1000));

Task.Run(() => { Console.WriteLine(1000); }, cts.Token);

  2.3.1 任務內部解密

  每個task對象都有一組字段,這些字段構成了任務的狀態。其中包括一個Int32 Id(只讀)、代表task執行狀態的一個int32、對父任務的引用、對task創建時指定的taskScheduler的引用、對回調方法的引用、對要傳給回調方法的對象的引用(課通過task的只讀asyncState屬性查詢)、對ExecutionContext的引用以及對ManualResetEventSlim對象的引用。另外每個task對象都有根據需要創建補充狀態的引用。補充狀態包含CancellationToken、一個ContinueWithTask對象集合、未拋出未處理異常的子任務而準備的一個task對象集合等。以上這麽多,讓我們意識到task雖然有用,但是並不是沒有代價,如果不需要task的附加功能,那麽使用threadpool.QueueUserWorkItem能獲得更好的資源利用率。

在一個task對象的存在期間,課查詢task的只讀status屬性了解它在其生存期的什麽位置。該屬性返回一個taskStatus值,如下

技術分享圖片

首次構造task對象時,他的狀態是created。以後,當任務啟動時,他的狀態變成waitingToRun。task實際在一個線程上運行時,他的狀態變成running。任務停止運行,並等待他的任何子任務時,狀態變成waitingForChildrenToComplete。任務完成時進入一下狀態之一:RanToCompoletion(運行完成),Canceled(取消)或Faulted(出錯)。如果運行完成,可通過task<TResult>的Result屬性來查詢任務結果。出錯時,可查詢task的exception屬性來獲取任務拋出的未處理異常,該屬性總是返回一個aggregateException對象,對象的innerException集合包含了所有未處理的異常。

為了簡化編碼,task提供了幾個只讀Boolean屬性,包括IsCanceled,IsFaulted和IsCompleted。

調用continueWith等方法創建的task對象處於waitingForActivation狀態。該狀態意味著task的調度由任務基礎結構控制,自動啟動。

  2.3.2 任務工廠

  有時候需要創建一組共享相同配置的task對象。為避免機械的賦值,我們可以創建一個任務工廠來封裝通用配置,TaskFactory和TaskFactory<TResult>。創建工廠類,需要向構造器傳遞需要具有的默認值,如CancellationToken、TaskScheduler、TaskCreationOptions及TaskContinuationOptions等。

實例代碼如下

技術分享圖片
public static void Go()
{
    Task parent = new Task(() =>
    {
        var cts = new CancellationTokenSource();
        var tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
        var childTasks = new[] {
            tf.StartNew(()=> Sum(cts.Token,10000)),
            tf.StartNew(()=> Sum(cts.Token,20000)),
            tf.StartNew(()=> Sum(cts.Token,Int32.MaxValue))//這裏執行會拋錯
        };
        //任何子任務拋出異常,就取消其余子任務
        for (int task = 0; task < childTasks.Length; task++)
        {
            childTasks[task].ContinueWith(t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted);
        }
        //所有子任務完成後,從未出錯/未取消的任務獲取返回的最大值,
        //然後將最大值傳給另一個任務來顯示最大結果
        tf.ContinueWhenAll(childTasks, completedTasks => completedTasks.Where(t => !t.IsFaulted && !t.IsCanceled).Max(t => t.Result), CancellationToken.None).ContinueWith(t => Console.WriteLine("The maximum is :" + t.Result), TaskContinuationOptions.ExecuteSynchronously);
    });
    //子任務完成後,顯示任何未處理的異常
    parent.ContinueWith(p =>
    { //這裏先用stringbuilder收集輸入,然後調用一次Console.WriteLine()
        StringBuilder sb = new StringBuilder("the following exception(s) occurred:" + Environment.NewLine);
        foreach (var e in p.Exception.Flatten().InnerExceptions)
        {
            sb.Append(" " + e.GetType().ToString());
        }
        Console.WriteLine(sb.ToString());
    }, TaskContinuationOptions.OnlyOnFaulted);
parent.Start();
parent.Wait();
}
private static Int32 Sum(CancellationToken ct, Int32 n)
{
    Int32 sum = 0;
    for (; n>0; n--)
    {
        ct.ThrowIfCancellationRequested();
        checked
        {
            sum += n;
        }
    }
    return sum;
}
任務工廠代碼

  2.3.3 任務調度

  任務基礎結構非常靈活,TaskScheduler對象功不可沒。TaskScheduler賦值執行任務的調度,同時向visual studio調試器公開任務信息。fcl提供了兩個派生自TaskScheduler的類型:線程池任務調度器(thread pool task scheduler),和同步上下文任務調度器(synchronization context task scheduler)。默認情況下都是使用線程池任務調度器。同步上下文任務調度器適合提供了圖形用戶界面的應用程序,如wpf,uwp等。

參考資料:

《CLR via C#(第四版)》

MSDN

Stephen Cleary相關異步文章

第一篇文章,所以先把基礎的東西寫出來,後續會深入討論異步編程的實踐。

C#異步編程(一)線程及異步編程基礎