.NET面試題系列[17] - 多線程概念(2)

線程概念

線程和進程的區別

  1. 進程是應用程序的一個實例要使用的資源的一個集合。進程經過虛擬內存地址空間進行隔離,確保各個進程之間不會相互影響。同一個進程中的各個線程之間共享進程擁有的全部資源。
  2. 線程是系統調度的基本單位。時間片和線程相關,和進程無關。
  3. 一個進程至少要擁有一個前臺線程。

線程開銷

當咱們建立了一個線程後,線程裏面主要包括線程內核對象、線程環境塊、1M大小的用戶模式棧和內核模式棧。程序員

  1. 線程內核對象:若是是內核模式構造的線程,則存在一個線程內核對象,包含一組對線程進行描述的屬性,以及線程上下文(包含了CPU寄存器中的數據,用於上下文切換)。
  2. 線程環境塊:用戶模式中分配和初始化的一個內存塊。
  3. 用戶模式棧:對於用戶模式構造的線程,應用程序能夠直接和用戶模式棧溝通。
  4. 內核模式棧:若是是內核模式構造的線程進行上下文切換和其餘操做時,須要調用操做系統的函數。此時須要使用內核模式棧向操做系統的函數傳遞參數。應用程序代碼沒法直接訪問內核模式棧,它須要藉助用戶模式的代碼。

線程有本身的線程棧,大小爲1M,因此它能夠維護本身的變量。線程是一個新的對象,它會增長系統上下文切換的次數,因此過多的線程將致使系統開銷很大。例如outlook會建立38個線程,但大部分時候他什麼都不作。因此咱們白白浪費了38M的內存。cookie

單核CPU一次只能作一件事,因此係統必須不停的進行上下文切換,且全部的線程(邏輯CPU)之間共享物理CPU。在某一時刻,系統只將一個線程分配給一個CPU。而後,該線程能夠運行一個時間片(大約30毫秒),過了這段時間,就發生上下文切換到另外一個線程。多線程

假設某個應用程序的線程進入無限循環,系統會按期搶佔他(不讓他再次運行)而容許新線程運行一會。若是新線程剛好是任務管理器的線程(此時將會發現任務管理器能夠響應,而任務管理器以外屏幕其餘地方則仍然無響應),則用戶能夠利用任務管理器殺死包含了其餘已經凍結的線程的進程。經過這種作法,上下文切換開銷並不會帶來任何性能增益,但換來了好得多的用戶體驗(很難死機,用戶能夠用任務管理器殺死其餘的進程)。閉包

當某個線程一直空閒(例如一個開啓的記事本但長時間無輸入)時,他能夠提早終止屬於他的時間片。線程也能夠進入掛起狀態,此時以後任什麼時候間片,都不會分配到這個線程,除非發生了某個事件(例如用戶進行了輸入)。節省出來的時間可讓CPU調度其餘線程,加強系統性能。app

線程的狀態

能夠用下圖表示:異步

線程的主要狀態有四種:就緒(Unstarted),運行(Running),阻塞(WaitSleepJoin)和中止(Stopped),還有一種Aborted就是被殺死了。一般,強制得到線程執行任務的結果,或者經過鎖等同步工具,會令線程進入阻塞狀態。當獲得結果以後,線程就解除阻塞,回到就緒狀態。ide

當創建一個線程時,它的狀態爲就緒。使用Start方法令線程進入運行狀態。此時線程就開始執行方法。若是沒有遇到任何問題,則線程執行完方法以後,就進入中止狀態。函數

阻塞(WaitSleepJoin),顧名思義,是使線程進入阻塞狀態。當一個線程被阻塞以後,它馬上用盡它的時間片(即便還有時間),而後CPU將永遠不會調度時間片給它直到它解除阻塞爲止(在將來的多少毫秒內我不參與CPU競爭)。主要方式有:Thread.Join(其餘線程都運行完了以後就解除阻塞),Thread.Sleep(時間到了就解除阻塞),Task.Result(獲得結果了就解除阻塞),遭遇鎖而拿不到鎖的控制權(等到其餘線程釋放鎖,本身拿到鎖,就解除阻塞)等。固然,自旋也是阻塞的一種。工具

Thread類中的方法對線程狀態的影響

Start:使線程從就緒狀態進入運行狀態性能

Sleep:使線程從運行狀態進入阻塞狀態,持續若干時間,而後阻塞自動解除回到運行狀態

Join:使線程從運行狀態進入阻塞狀態,當其餘線程都結束時阻塞解除

Interrupt:當線程被阻塞時,即便阻塞解除的要求尚未達到,可使用Interrupt方法強行喚醒線程使線程進入運行狀態。這將會引起一個異常。(例如休息10000秒的線程能夠被馬上喚醒)

Abort:使用Abort方法能夠強行殺死一個處於任何狀態的線程

時間片

當咱們討論多任務時,咱們指出操做系統爲每一個程序分配必定時間,而後中斷當前運行程序並容許另一個程序執行。這並不徹底準確。處理器實際上爲進程分配時間。進程能夠執行的時間被稱做「時間片」或者「限量」。時間片的間隔對程序員和任何非操做系統內核的程序來講都是變化莫測的。程序員不該該在他們的程序中將時間片的值假定爲一個常量。每一個操做系統和每一個處理器均可能設定一個不一樣的時間。

進程和線程優先級

Windows是一個搶佔式的操做系統。在搶佔式操做系統中,較高優先級的進程老是搶佔(preempt較低優先級的進程(即便時間片沒有用完)。用戶不能保證本身的線程一直運行,也不能阻止其餘線程的運行。 

每個進程有一個優先級類,每個線程有一個優先級(0-31)。較高優先級的進程中的較高優先級的線程得到優先分配時間片的權利。

只要存在能夠調度的高優先級的線程,系統就永遠不會將低優先級的現場分配給CPU,這種狀況稱爲飢餓。飢餓應該儘可能避免,可使用不一樣的調度方式,而不是僅僅看優先級的高低。在多處理器機器上飢餓發生的可能性較小些,由於這種機器上,高優先級的線程和低優先級的線程能夠同時運行。

Thread類中的Priority容許用戶改變線程的優先級(但不是直接指定1-31之間的數字,而是指定幾個層級,每一個層級最終mapping到數字,例如層級normal會映射到4)

前臺和後臺線程

一個進程能夠有任意個前臺和後臺線程。前臺線程使得整個進程得以繼續下去。一個進程的全部前臺線程都結束了,進程也就結束了。當該進程的全部前臺線程終止時,CLR將強制終止該進程的全部後臺線程,這將會致使finally可能沒來得及執行(從而致使一些垃圾回收的問題)。解決的方法是使用join等待。例如你在main函數中設置了一個後臺線程,而後讓其運行,假設它將運行較長的時間,而此後main函數就沒有代碼了,那麼程序將馬上終止,由於main函數是後臺線程。

使用thread類建立的線程默認都是前臺線程。Thread的IsBackground類容許用戶將一個線程置爲後臺線程。

多線程有什麼好處和壞處?

好處:

  1. 更大限度的利用CPU和其餘計算機資源。
  2. 當一條線程凍結時,其餘線程仍然能夠運行。
  3. 在後臺執行長任務時,保持用戶界面良好的響應。
  4. 並行計算(僅當這麼作的好處大於對資源的損耗時)

壞處:

  1. 線程的建立和維護須要消耗計算機資源。(使用線程池,任務來抵消一部分損失)。一條線程至少須要耗費1M內存。
  2. 多個線程之間若是不一樣步,結果將會難以預料。(使用鎖和互斥)
  3. 線程的啓動和運行時間是不肯定的,由系統進行調度,因此可能會形成資源爭用,一樣形成難以預料的結果。(使用鎖和互斥,或者進行原子操做)

爲了不2和3,須要開發者更精細的測試代碼,增長了開發時間。

System.Threading類的基本使用

建立線程

可使用Thread的構造函數建立線程。咱們要傳遞一個方法做爲構造函數的參數。一般咱們能夠傳遞ThreadStart委託或者ParameterizedThreadStart委託。後者是一個能夠傳遞輸入參數的委託。兩個委託都沒有返回值。ThreadStart委託的簽名是:public delegate void ThreadStart();

1 基本例子:經過Thread構造函數創建一個線程。傳遞的方法WriteY沒有返回值,也沒有輸入。以後使用Start方法使線程開始執行任務WriteY。

class ThreadTest
{
  static void Main()
  {
    Thread t = new Thread (WriteY);          
    t.Start();                            
 
    for (int i = 0; i < 1000; i++) Console.Write ("x");
  }
 
  static void WriteY()
  {
    for (int i = 0; i < 1000; i++) Console.Write ("y");
  }
}
View Code

這個例子中,主線程和次線程同時訪問一個靜態方法(靜態方法是類級別的)。此時系統調度使得主線程和次線程輪流運行(但運行的順序是隨機的)。因此結果多是

xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
yyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

2 主線程和次線程分別維護各自的局部變量

static void Main()
{
  new Thread (Go).Start();    
  Go();                         
}
 
static void Go()
{
  // Declare and use a local variable - 'cycles'
  for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?');
}
View Code

次線程有本身的線程棧(大小1兆),因此主線程和次線程分別擁有各自的局部變量cycles。結果將是十個問號。這十個問號出自主線程和次線程,順序不定。

3 主線程和次線程分享全局變量

class ThreadTest
{
  bool done;
 
  static void Main()
  {
    ThreadTest tt = new ThreadTest();   // Create a common instance
    new Thread (tt.Go).Start();
    tt.Go();
  }
 
  // Note that Go is now an instance method
  void Go() 
  {
     if (!done) { done = true; Console.WriteLine ("Done"); }
  }
}
View Code

變量done是全局的,被全部線程共享。此時,次線程開始任務,並在Go方法中將done設爲真。最後只會打印一個done。

什麼時候考慮建立一個線程?

  1. 當建立線程的代價比線程池要小(例如只打算建立一個線程時)
  2. 當但願本身管理線程的優先級時(線程池自動管理)
  3. 須要一個前臺線程(線程池建立的線程都是後臺的)

向次線程傳遞數據

1. 使用Lambda表達式。此時仍然使用的是ThreadStart委託。

static void Main()
{
  Thread t = new Thread ( () => Print ("Hello from t!") );
  t.Start();
}
 
static void Print (string message) 
{
  Console.WriteLine (message);
}
View Code

 

2. 使用Thread的另外一個構造函數傳入一個ParameterizedThreadStart委託

ParameterizedThreadStart委託的簽名是:public delegate void ParameterizedThreadStart (object obj);

因此它只能傳遞object類型的數據而且不能有返回值。

static void Main()
{
  Thread t = new Thread (Print);
  t.Start ("Hello from t!");
}
 
static void Print (object messageObj)
{
  string message = (string) messageObj;   // We need to cast here
  Console.WriteLine (message);
}
View Code

捕獲變量問題

因爲lambda表達式造成閉包,致使有機會出現捕獲變量。

for (int i = 0; i < 10; i++)
  new Thread (() => Console.Write (i)).Start();
View Code

上例的捕獲變量:全世界只有一個i,因此被十條線程共用。

上面的代碼造成了閉包,致使i成爲捕獲變量被十個匿名函數共享。出來的結果將是沒法預料的。解決方法是在表達式內部聲明變量,這將是匿名函數本身的變量。(此時循環增長一次就有一個temp因此每一個線程有本身的變量)

for (int i = 0; i < 10; i++)
{
  int temp = i;
  new Thread (() => Console.Write (temp)). Start();
}
View Code

Join:阻塞的是呼叫的線程

封鎖呼叫的線程,直到其餘線程結束爲止。定義十分費解,看看例子。

例子1:Join阻塞的是呼叫的線程,在這個例子中呼叫的線程就是主線程。此時主線程將不會運行最後一行,直到次線程打印完了1000個y爲止。

若是沒有Join,則程序將馬上退出。

static void Main()
{
  Thread t = new Thread (Go);
  t.Start();
  t.Join();
  Console.WriteLine ("Thread t has ended!");
}
 
static void Go()
{
  for (int i = 0; i < 1000; i++) Console.Write ("y");
}
View Code

例子2:等待

static void Main(string[] args)
        {
            Thread t1 = new Thread(PrintOne);
            Thread t2 = new Thread(PrintTwo);
            Thread t3 = new Thread(PrintThree);
            t1.Start();
            t2.Start();
            t2.Join(); //等待其餘線程運行完畢(這裏只有t1須要等待)
            t1.Join();
            t3.Start();
            Console.ReadKey();
        }

        static void PrintOne()
        {
            Console.WriteLine("One");
        }
        static void PrintTwo()
        {
            Console.WriteLine("Two");
        }
        static void PrintThree()
        {
            Console.WriteLine("Three");
        }
View Code

將按順序打印One, Two, Three。t2.Join()阻塞呼叫的線程t2,因而等待t1運行完畢。T1.Join()則沒有要等待的線程。

Join能夠設置一個timeout時間。

Sleep

讓線程中止一段時間。呼叫Sleep或Join將阻塞線程,系統將不會爲其分配時間片,因此不會耗費系統性能。特別的,Sleep(0)會將線程如今的時間片馬上用盡(即便還有剩餘的時間)。

線程池

線程池是由CLR自動管理的,包含若干線程的集合。CLR利用線程池自動進行多線程中線程的建立,執行任務和銷燬。利用任務或委託,能夠隱式的和線程池發生關聯。

線程池是如何管理線程的?

線程池的工做方法和普通的線程有所不一樣。他維護一個隊列QueueUserWorkItem,當程序想執行一個異步操做時,線程池將這個操做追加到隊列中,並派遣給一個線程池線程。線程池建立伊始是沒有線程的。若是線程池中沒有線程,就建立一個新線程。

相對於普通的使用Threading類建立線程,線程池的好處有:

  1. 線程池中建立的線程不會在執行任務以後銷燬,而是返回線程池等待下一個響應,這樣咱們能夠最大限度的重用線程。
  2. 線程池會盡可能用最少的線程處理隊列中的全部請求,只有在隊列增長的速度超過了請求處理的速度以後,線程池纔會考慮建立線程。
  3. 若是線程池中的線程空閒了一段時間,它會本身醒來終止本身以釋放資源。
  4. 當同時運行的線程超過閾值時,線程池將不會繼續開新的線程,而是等待現有的線程運行完畢。

線程池的缺點:

  1. 你不能爲線程命名
  2. 線程池建立的線程必定是後臺線程

C#運用了線程池的類和操做有:

  1. 任務並行庫
  2. 委託
  3. BackgroundWorker

等等。

使用線程池:經過任務

咱們能夠經過建立一個任務來隱式的使用線程池:

static void Main()    // The Task class is in System.Threading.Tasks
{
  Task.Factory.StartNew (Go);
}
 
static void Go()
{
  Console.WriteLine ("Hello from the thread pool!");
}
View Code

任務方法能夠有返回值,咱們能夠經過訪問Task.Result(會阻塞)來獲得這個返回值。當訪問時,若是任務執行中出現了異常,則咱們能夠將訪問Task.Result寫入try塊來捕捉異常。

使用線程池:顯式操做

咱們能夠經過顯式操做ThreadPool.QueueUserWorkItem隊列來操縱線程池,爲它添加任務。咱們還可使用其的重載爲任務指派輸入變量。

static void Main()
{
  ThreadPool.QueueUserWorkItem (Go);
  ThreadPool.QueueUserWorkItem (Go, 123);
  Console.ReadLine();
}
 
static void Go (object data)   
{
  Console.WriteLine ("Hello from the thread pool! " + data);
}
View Code

和任務有所不一樣,ThreadPool.QueueUserWorkItem的方法沒法有返回值。並且,必須在方法的內部進行異常處理,不然將會出現執行時異常。

使用線程池:異步委託

異步委託是一種解決ThreadPool.QueueUserWorkItem沒有返回值的方法。

static void Main()
{
  Func<string, int> method = Work;
  IAsyncResult cookie = method.BeginInvoke ("test", null, null);
  //
  // ... here's where we can do other work in parallel...
  //
  int ret = method.EndInvoke (cookie);
  Console.WriteLine ("String length is: " + ret);
}
 
static int Work (string s) { return s.Length; }
View Code

異步調用一個方法也至關於給線程池派了一個新的任務。咱們能夠經過訪問method.EndInvoke來得到訪問結果。

相關文章
相關標籤/搜索