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

來源:互聯網
上載者:User

標籤:eset   env   exec   sed   好的   source   creat   屬性查詢   gate   

  最近試著做了幾個.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#非同步編程(一)線程及非同步編程基礎

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.