CLR via C# 讀書筆記-27.計算限制的異步操作(上篇)
前言
學習這件事情是一個習慣,不能停。。。另外這篇已經看過兩個月過去,但覺得有些事情不總結跟沒做沒啥區別,遂記下此文
1.CLR線程池基礎
2.ThreadPool的簡單使用練習
3.執行上下文
4.協作式取消和超時,System.Threading.CancellationTokenSource的簡單使用
5.任務
6.任務調度器
一、CLR線程池基礎
如26章所述,創建和銷毀線程是一個昂貴的操作,要耗費大量的時間。另外太多的線程會浪費內存資源。由於操作系統必須調度可運行的線程並執行上下文切換,所以太多的線程還對性能不利。
為了改善這個情況,CLR包含了代碼管理它自己的線程池(thread pool),線程池是你的應用程序能使用的線程的集合。
每CLR一個線程池,這個線程池由CLR控制的所有AppDomain共享。
CLR初始化時,線程池中是沒有線程的。在內部,線程池維護了一個操作請求隊列。應用程序執行一個異步操作時,就調用某個方法,將一個記錄項(entry)追加到線程池的隊列中,線程池的代碼從這個隊列中提取記錄項,將這個記錄項派發(dispatch)給一個線程池線程。如果線程池中沒有線程,就創建一個新線程。
如果應用程序向線程池發出許多請求,線程池會嘗試只用一個線程來服務所有請求。然而,如果你的應用程序發出請求的速度超過了線程池線程處理它們的速度,就會創建額外的線程。
當一個線程池線程閑著沒事一段時間之後,線程會自己醒來終止自己以釋放資源。
二、ThreadPool的簡單使用練習
class Program { static void Main(string[] args) { Console.WriteLine($"Main Thread,當前線程:{Thread.CurrentThread.ManagedThreadId}"); ThreadPool.QueueUserWorkItem(Calculate,5); Console.WriteLine($"Main Thread doing other work,當前線程:{Thread.CurrentThread.ManagedThreadId}View Code"); Thread.Sleep(1000); Console.WriteLine("hi <Enter> to end this program~~"); Console.Read(); } //這個方法的簽名必須匹配waitcallback委托 public static void Calculate(object state) { //這個方法由一個線程池線程執行 Console.WriteLine($"In Calculate:state={state},當前線程:{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); //這個方法返回後,線程回到池中,等待另一個任務 } }
運行結果:
有時上圖標註這兩行輸出結果順序會顛倒,這是因為兩個方法相互之間是異步運行的,windows調度器決定先調度哪一個線程。
三、執行上下文
每個線程都關聯一個執行上下文數據結構。
執行上下文(execution context)包括的東西有安全設置(壓縮棧、Thread的Principal屬性和Windows的身份)、宿主設置(System.Threading.HostExecutionContextManager)以及邏輯調用上下文數據(參見System.Runtime.Remoting.Messaging.CallContext的LogicalSetData和LogicalGetData方法)。
默認情況下,CLR自動造成初始線程的執行上下文“流向”任何輔助線程。這造成將上下文信息傳給輔助線程,但這會對性能造成一定影響。
這是因為執行上下文中包含大量信息,而收集所有這些信息,再把它們復制到輔助線程,要耗費不少時間。
System.Threading.ExecutionContext類,允許你控制線程的執行上下文如何從一個線程“流向”另一個。可用這個類 阻止上下文流動以提升應用程序的性能。
class Program { static void Main(string[] args) { //將一些數據放到Main線程的邏輯調用上下文中 CallContext.LogicalSetData("Name", "Michael"); //初始化要由線程池線程做的一些工作 //線程池線程能訪問邏輯調用上下文結構 ThreadPool.QueueUserWorkItem( state => Console.WriteLine($"Name={CallContext.LogicalGetData("Name")}")); //阻止Main線程的執行上下文的流動 ExecutionContext.SuppressFlow(); //初始化要由線程池做的工作 //線程池線程不能訪問邏輯調用上下文數據 ThreadPool.QueueUserWorkItem( state => Console.WriteLine($"Name={CallContext.LogicalGetData("Name")}")); //恢復Main線程的執行上下文的流動, //以免將來使用更多的線程池線程 ExecutionContext.RestoreFlow(); Console.ReadLine(); } }View Code
編譯後運行結果如下:
四、協作式取消和超時,System.Threading.CancellationTokenSource的簡單使用
Microsoft.NET Framework提供了標準的取消操作模式。這個模式是協作式的,意味著要取消的操作必須顯式支持取消。
CancellationToken實例是輕量級值類型,包含單個私有字段,即對其CancellationTokenSource對象的引用。
在計算限制操作的循環中,可定時調用CancellationToken的IsCancellationRequsted屬性,了解循環是否應該提前終止,從而終止計算限制的操作。
提前終止的好處在於,CPU不需要再把時間浪費在你對結果不感興趣的操作上。
static void Main(string[] args) { Go(); } public static void Go() { CancellationTokenSource token = new CancellationTokenSource(); //將CancellationTokenSource和參數 傳入操作 ThreadPool.QueueUserWorkItem( o => Count(token,1000)); Console.WriteLine($"Hit <Enter> to cancel operation"); Console.ReadLine(); token.Cancel();//如果Count方法已返回,Cancel沒有任何效果 //執行cancel後 立即返回,方法從這裏繼續運行 Console.ReadLine(); } public static void Count(CancellationTokenSource token,Int32 counto) { for (int count = 0; count < counto; count++) { if(token.IsCancellationRequested) { Console.WriteLine("操作被取消"); break; } Console.WriteLine(count); Thread.Sleep(200); //出於顯示目的而浪費一些時間你 } Console.WriteLine("Count is done"); }View Code
運行結果如下圖所示:
可調用CancellationTokenSource的Register方法登記一個或多個在取消一個CancellationTokenSource時調用的方法。
向被取消的CancellationTokenSource登記一個回調方法,將由調用Register的線程調用回調方法(如果為useSynchronizationContext參數傳遞了true值,就可能要通過調用線程的SynchronizationContext進行)。
多次調用Register,多個調用方法都會調用。這些回調方法可能拋出未處理的異常。
如果調用CancellationTokenSource的Cancel方法,向它傳遞true,那麽拋出了未處理異常的第一個回調方法會阻止其他回調方法的執行,拋出的異常也會從Cancel中拋出。
如果調用Cancel並向它傳遞false,那麽登記的所有回調方法都會調用。所有未處理的異常都會添加到一個集合中。所有回調方法都執行好後,其中任何一個拋出了未處理的異常,Cancel就會拋出一個AggregateException,該異常實例的InnerExceptions屬性被設為已拋出的所有異常對象的集合。
static void Main(string[] args) { var cts1 = new CancellationTokenSource(); cts1.Token.Register(() => Console.WriteLine($"cts1被取消")); var cts2 = new CancellationTokenSource(); cts2.Token.Register(() => Console.WriteLine($"cts2被取消")); var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token); linkedCts.Token.Register(() => Console.WriteLine($"linkedCts 被取消")); cts2.Cancel(); Console.WriteLine($"cts1 canceled={cts1.IsCancellationRequested},cts2 canceled={cts2.IsCancellationRequested}," + $"linkedCts={linkedCts.IsCancellationRequested}"); Console.ReadLine(); }View Code
運行結果如下圖:
如果要在過一段時間後取消操作,要麽用接收延時參數的構造器構造一個CancellationTokenSource對象,要麽調用CancellationTokenSource的CancelAfter方法。
五、任務
通過觀察,我們發現 ThreadPool最大的問題是沒有內建的機制讓你知道 操作在什麽時候完成,以及操作完成時獲得返回值。鑒於此,Microsoft引入了任務的概念。
下面展示一個使用task的簡單例子:
static void Main(string[] args) { Console.WriteLine($"當前線程ID:{Thread.CurrentThread.ManagedThreadId}"); //創建一個Task,現在還沒有開始運行 Task<Int32> t = new Task<int>(n => Sum((Int32)n), 10000); //可以後等待任務 t.Start(); //可選擇顯示等待任務完成 t.Wait(); //可獲得結果(result屬性內部會調用Wait) Console.WriteLine($"the Sum is:{t.Result},當前線程ID:{Thread.CurrentThread.ManagedThreadId}"); Console.ReadLine(); } private static Int32 Sum(Int32 n) { Int32 sum = 0; for (; n>0; n--)checked { sum += n; //如果n太大,會拋出System.OverflowException } Console.WriteLine($"In Sum,當前線程ID:{Thread.CurrentThread.ManagedThreadId}"); return sum; }View Code
運行結果如右圖:
如果計算限制任務拋出未處理的異常,異常會被“吞噬”並存儲到一個集合中,調用wait方法或Result屬性時,這些成員會拋出一個System.AggregateException對象。
AggregateException提供了一個Handle方法,它為AggregateException中包含的每個異常都調用一個回調方法。回調方法可以為每個異常決定如何對其處理;回調返回true表示異常已處理;返回false表示未處理。調用Handle後,如果至少有一個異常沒有處理,就創建一個新的AggregateException對象,其中只包含未處理的異常。
Task的靜態WaitAny方法會阻塞調用線程,直到數組中的任何Task對象完成。方法返回Int32數組索引值,指明完成的是哪個Task的對象
Task的靜態WaitAll方法也會阻塞調用線程,直到數組中的所有Task對象完成。
下面演示下task取消操作和task的異常處理
static void Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource(); Task<Int32> t = Task.Run(() => Sum(cts.Token, 10000), cts.Token); cts.Cancel(); try { Console.WriteLine($"the Sum is:{t.Result},當前線程ID:{Thread.CurrentThread.ManagedThreadId}"); } catch (AggregateException ex) { //將任何OperationCanceledException對象都是為已處理 //其他任何異常都造成拋出一個新的AggregateException //其中只包含未處理異常 ex.Handle(e => e is OperationCanceledException); Console.WriteLine("Sum was canceled"); } Console.ReadLine(); } private static Int32 Sum(CancellationToken ct, Int32 n) { Int32 sum = 0; for (; n>0; n--)checked { //再取消標誌引用的CancellationTokenSource上調用Cancel, //下面這行代碼就會拋出OperationCanceledException ct.ThrowIfCancellationRequested(); sum += n; //如果n太大,會拋出System.OverflowException } Console.WriteLine($"In Sum,當前線程ID:{Thread.CurrentThread.ManagedThreadId}"); return sum; }View Code
調用Wait,或者在任務尚未完成時查詢任務的Result屬性,極有可能造成線程池創建新線程,這增大了資源的消耗,也不利於性能和伸縮性。
要知道一個任務在什麽時候結束,任務完成時可啟動另一個任務。
Microsoft為我們提供了ContinueWith,下面簡單展示使用
static void Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource(); Task<Int32> t = Task.Run(() => Sum(cts.Token, 10000), cts.Token); Task cwt= t.ContinueWith(task => Console.WriteLine($"Sum result is {task.Result}")); } private static Int32 Sum(CancellationToken ct, Int32 n) { Int32 sum = 0; for (; n>0; n--)checked { //再取消標誌引用的CancellationTokenSource上調用Cancel, //下面這行代碼就會拋出OperationCanceledException ct.ThrowIfCancellationRequested(); sum += n; //如果n太大,會拋出System.OverflowException } Console.WriteLine($"In Sum,當前線程ID:{Thread.CurrentThread.ManagedThreadId}"); return sum; }View Code
Task對象內部包含了ContinueWith任務的一個集合。可在調用ContinueWith時傳遞對一組TaskContinuationOptions枚舉值進行判斷滿足什麽情況才執行ContinueWith。
偷個懶,哈哈。。。
任務可以啟動多個子任務,下面簡單展示下使用
static void Main(string[] args) { Task<Int32[]> task = new Task<Int32[]>(() => { var results = new Int32[3]; new Task(() => results[0] = Sum(1000), TaskCreationOptions.AttachedToParent).Start(); new Task(() => results[1] = Sum(1000), TaskCreationOptions.AttachedToParent).Start(); new Task(() => results[2] = Sum(1000), TaskCreationOptions.AttachedToParent).Start(); return results; }); var cwt = task.ContinueWith( parentTask => Array.ForEach(parentTask.Result, Console.WriteLine)); task.Start(); Console.ReadLine(); } private static Int32 Sum( Int32 n) { Int32 sum = 0; for (; n>0; n--)checked { sum += n; //如果n太大,會拋出System.OverflowException } Console.WriteLine($"In Sum,當前線程ID:{Thread.CurrentThread.ManagedThreadId}"); return sum; }View Code
TaskCreationOptions.AttachedToParrent標誌將一個Task和創建它的Task關聯,結果是除非所有子任務(以及子任務的子任務)結束運行,否則創建任務(父任務)不認為已經結束。
在一個Task對象的存在期間,可查詢Task的只讀Status屬性了解它在其生存期的什麽位置。
要創建一組共享相同配置的Task對象。可創建一個任務工廠來封裝通用的配置。即TaskFactory。
在調用TaskFactory或TaskFactory<TResult>的靜態ContinueWhenAll和ContinueWhenAny方法,無論前置任務是如何完成的,ContinueWhenAll和ContinueWhenAny都會執行延續任務。
六、任務調度器
對於不了解任務調度的小白來講,可能遇到過下面這個場景
啊,怎麽會這樣呢?為什麽不能在線程裏更新UI組件。
TaskScheduler對象負責執行被調度的任務,同時向Visual Studio調試器公開任務信息。
FCL提供了兩個派生自TaskScheduler的類型:線程池任務調度器(thread pool task scheduler),和同步上下文任務調度器(synchronization context task scheduler)。
默認情況下,所有應用程序使用的都是線程池任務調度器。可查詢TaskScheduler的靜態Default屬性來獲得對默認任務調度器的引用。
同步上下文任務調度器適合提供了圖形用戶界面的應用程序。它將所有任務都調度給應用程序的GUI線程,使所有任務代碼都能成功的更新UI組件。該調度不使用線程池。可執行TaskScheduler的靜態FromCurrentSynchronizationContext方法來獲得對同步上下文任務調度器的引用。
下面展示一個簡單的例子,演示如何使用同步上下文任務調度器
public partial class MainForm : Form { private readonly TaskScheduler m_syncContextTaskScheduler; public MainForm() { //獲得一個對同步上下文任務調度器的引用 m_syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); Text = "Synchronization Context Task Scheduler Demo"; Visible = true; Width = 400; Height = 400; } private CancellationTokenSource m_cts; protected override void OnMouseClick(MouseEventArgs e) { if(m_cts!=null) { m_cts.Cancel(); //一個操作正在運行,取消它 m_cts = null; } else { //任務沒有開始啟動它 Text = "Operation running"; m_cts = new CancellationTokenSource(); //這個任務使用默認任務調度器,在一個線程池線程上運行 Task<Int32> t = Task.Run(()=>Sum(1000),m_cts.Token); //這些任務使用 同步上下文任務調度器,在GUI線程上執行 t.ContinueWith(task => Text = "Result:" + t.Result, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, m_syncContextTaskScheduler); t.ContinueWith(task => Text = "Operation canceled ", CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled, m_syncContextTaskScheduler); t.ContinueWith(task => Text = "Operation defaulted ", CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, m_syncContextTaskScheduler); } base.OnMouseClick(e); } private static Int32 Sum(Int32 n) { Int32 sum = 0; for (; n > 0; n--) checked { sum += n; //如果n太大,會拋出System.OverflowException } return sum; } }View Code
單擊窗體的客戶區域,就會在線程池線程上啟動一個計算限制操作。使用線程池線程,因為GUI線程在此期間不會被阻塞,能響應其他UI操作。
Parallel就留在下篇來介紹吧。。。
順便講兩句廢話,2017 這一年真快,快的我來不及思考,似乎我就做了一件事,xx項目上線了,與此同時11月從臺北出差回來時,我向領導提出了退出該項目組,項目進展到了一個階段,也算是做人做事有頭有尾吧,並不是說 我放棄了,而是身在其位,就要有擔當,但實在有點累了,覺得不能夠做好後續事情,先暫且為公司做一些擴展工具的事情。這一年也經歷了前前後後,其中原始的成員4個離職了,2個去了廣州出差,踩得坑一個接一個。具體會在另一篇博客中總結一下這一年的項目經驗,開發經驗,以及見識過的高人身上發光點(早該寫了)。。。
深知自己見識、修為淺薄,2018,忌焦躁,忌功利。
要做的事情:
1.業余時間 用net core做一兩個項目
2.參加開源社區項目,回饋社區,不再是一直索取
3.CLR這本書在年前看完並理解
4.接觸一下GO,Docker
5.拓展看一些與專業無關的書,比如 暗時間等(一直想看老是忘記,記下來。。。)
6.鍛煉身體,註意健康,調整作息,老是熬夜不行
好幾次,看到骨仔早上5點都起床了,深感差距好大,好大。啊哈哈。。。
CLR via C# 讀書筆記-27.計算限制的異步操作(上篇)