1. 程式人生 > 實用技巧 >.NET基礎知識-程序與執行緒

.NET基礎知識-程序與執行緒

執行緒基礎

  我們執行一個exe,就是一個程序例項,系統中有很多個程序。每一個程序都有自己的記憶體地址空間,每個程序相當於一個獨立的邊界,有自己的獨佔的資源,程序之間不能共享程式碼和資料空間。

  

  每一個程序有一個或多個執行緒,程序內多個執行緒可以共享所屬程序的資源和資料,執行緒是作業系統排程的基本單元。執行緒是由作業系統來排程和執行的,她的基本狀態如下圖:

  

執行緒的開銷及排程

  當我們建立了一個執行緒後,執行緒裡面到底有些什麼東西呢?主要包括執行緒核心物件、執行緒環境塊、1M大小的使用者模式棧、核心模式棧。其中使用者模式棧對於普通的系統執行緒那1M是預留的,在需要的時候才會分配,但是對於CLR執行緒,那1M是一開始就分類了記憶體空間的。

補充一句,CLR執行緒是直接對應於一個Windows執行緒的。

  

  計算機的核心計算資源就是CPU核心和CPU暫存器,這也就是執行緒執行的主要戰場。作業系統中那麼多執行緒(一般都有上千個執行緒,大部分都處於休眠狀態),對於單核CPU,一次只能有一個執行緒被排程執行,那麼多執行緒怎麼分配的呢?Windows系統採用時間輪詢機制,CPU計算資源以時間片(大約30ms)的形式分配給執行執行緒。

  計算機資源(CPU核心和CPU暫存器)一次只能排程一個執行緒,具體的排程流程:

  • 把CPU暫存器內的資料儲存到當前執行緒內部(執行緒上下文等地方),給下一個執行緒騰地方;
  • 執行緒排程:線上程集合裡取出一個需要執行的執行緒;
  • 載入新執行緒的上下文資料到CPU暫存器;
  • 新執行緒執行,享受她自己的CPU時間片(大約30ms),完了之後繼續回到第一步,繼續輪迴;

  上面執行緒排程的過程,就是一次執行緒切換,一次切換就涉及到執行緒上下文等資料的搬入搬出,效能開銷是很大的。因此執行緒不可濫用,執行緒的建立和消費也是很昂貴的,這也是為什麼建議儘量使用執行緒池的一個主要原因。

  對於Thread的使用太簡單了,這裡就不重複了,總結一下執行緒的主要幾點效能影響:

  • 執行緒的建立、銷燬都是很昂貴的;
  • 執行緒上下文切換有極大的效能開銷,當然假如需要排程的新執行緒與當前是同一執行緒的話,就不需要執行緒上下文切換了,效率要快很多;
  • 這一點需要注意,GC執行回收時,首先要(安全的)掛起所有執行緒,遍歷所有執行緒棧(根),GC回收後更新所有執行緒的根地址,再恢復執行緒呼叫,執行緒越多,GC要乾的活就越多;

  當然現在硬體的發展,CPU的核心越來越多,多執行緒技術可以極大提高應用程式的效率。但這也必須在合理利用多執行緒技術的前提下,了執行緒的基本原理,然後根據實際需求,還要注意相關資源環境,如磁碟IO、網路等情況綜合考慮。

多執行緒

  單執行緒的使用這裡就略過了。上面總結了執行緒的諸多不足,因此微軟提供了可供多執行緒程式設計的各種技術,如執行緒池、任務、並行等等。

  執行緒池ThreadPool

  執行緒池的使用是非常簡單的,如下面的程式碼,把需要執行的程式碼提交到執行緒池,執行緒池內部會安排一個空閒的執行緒來執行你的程式碼,完全不用管理內部是如何進行執行緒排程的。

ThreadPool.QueueUserWorkItem(t => Console.WriteLine("Hello thread pool"));

  每個CLR都有一個執行緒池,執行緒池在CLR內可以多個AppDomain共享,執行緒池是CLR內部管理的一個執行緒集合,初始是沒有執行緒的,在需要的時候才會建立。執行緒池的主要結構,基本流程如下:

  • 執行緒池內部維護一個請求列隊,用於快取使用者請求需要執行的程式碼任務,就是ThreadPool.QueueUserWorkItem提交的請求;
  • 有新任務後,執行緒池使用空閒執行緒或新執行緒來執行佇列請求;
  • 任務執行完後執行緒不會銷燬,留著重複使用;
  • 執行緒池自己負責維護執行緒的建立和銷燬,當執行緒池中有大量閒置的執行緒時,執行緒池會自動結束一部分多餘的執行緒來釋放資源;

  執行緒池是有一個容量的,因為他是一個池子嘛,可以設定執行緒池的最大活躍執行緒數,呼叫方法ThreadPool.SetMaxThreads可以設定相關引數。但很多程式設計實踐裡都不建議程式猿們自己去設定這些引數,其實微軟為了提高執行緒池效能,做了大量的優化,執行緒池可以很智慧的確定是否要建立或是消費執行緒,大多數情況都可以滿足需求了。

執行緒池使得執行緒可以充分有效地被利用,減少了任務啟動的延遲,也不用大量的去建立執行緒,避免了大量執行緒的建立和銷燬對效能的極大影響。

  上面瞭解了執行緒的基本原理和諸多優點後,如果你是一個愛思考的猿類,應該會很容易發現很多疑問,比如把任務新增到執行緒池佇列後,怎麼取消或掛起呢?如何知道她執行完了呢?下面來總結一下執行緒池的不足

  • 執行緒池內的執行緒不支援執行緒的掛起、取消等操作,如想要取消執行緒裡的任務,.NET支援一種協作式方式取消,使用起來也不少很方便,而且有些場景並不滿足需求;
  • 執行緒內的任務沒有返回值,也不知道何時執行完成;
  • 不支援設定執行緒的優先順序,還包括其他類似需要對執行緒有更多的控制的需求都不支援;

  因此微軟為我們提供了另外一個東西叫做Task來補充執行緒池的某些不足。

  任務Task與並行Parallel

  任務Task與並行Parallel本質上內部都是使用的執行緒池,提供了更豐富的並行程式設計的方式。任務Task基於執行緒池,可支援返回值,支援比較強大的任務執行計劃定製等功能,下面是一個簡單的示例。Task提供了很多方法和屬性,通過這些方法和屬效能夠對Task的執行進行控制,並且能夠獲得其狀態資訊。Task的建立和執行都是獨立的,因此可以對關聯操作的執行擁有完全的控制權。

//建立一個任務
Task<int> t1 = new Task<int>(n =>
{
    System.Threading.Thread.Sleep(1000);
    return (int)n;
}, 1000);
//定製一個延續任務計劃
t1.ContinueWith(task =>
{
    Console.WriteLine("end" + t1.Result);
}, TaskContinuationOptions.AttachedToParent);
t1.Start();
//使用Task.Factory建立並啟動一個任務
var t2 = System.Threading.Tasks.Task.Factory.StartNew(() =>
{
    Console.WriteLine("t1:" + t1.Status);
});
Task.WaitAll();
Console.WriteLine(t1.Result);

  並行Parallel內部其實使用的是Task物件(TPL會在內部建立System.Threading.Tasks.Task的例項),所有並行任務完成後才會返回。少量短時間任務建議就不要使用並行Parallel了,並行Parallel本身也是有效能開銷的,而且還要進行並行任務排程、建立呼叫方法的委託等等。