C# 線程入門 00

來源:互聯網
上載者:User

標籤:支援   ons   from   locker   windows服務   進程和線程   技術分享   分享   art   

內容預告:

  • 線程入門(線程概念,建立線程)
  • 同步基礎(同步本質,安全執行緒,線程中斷,線程狀態,同步上下文)
  • 使用線程(背景工作,線程池,讀寫鎖,非同步代理程式,定時器,本機存放區)
  • 進階話題(非阻塞線程,扶起和恢複)

概覽:

C#支援通過多線程並行地執行代碼,一個線程是獨立的執行個體,可以和其他線程同時運行。

CLR和作業系統會給C#程式開啟一個線程(主線程),可以被用來作為建立多線程的起點,例子:

class ThreadTest {static void Main() {Thread t = new Thread (WriteY);t.Start(); // Run WriteY on the new threadwhile (true) Console.Write ("x"); // Write ‘x‘ forever}static void WriteY() {while (true) Console.Write ("y"); // Write ‘y‘ forever}}

運行結果將是:

xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

主線程建立了一個線程 t ,執行了重複輸出y的操作,主要線程執行了重複輸出x的操作。
CLR給每個線程分配了單獨的線程棧,所以本地變數是每個線程單獨儲存的,下面的例子,我們用一個本地變數定義一個函數,然後在main函數和新的線程裡同時執行這個函數

static void Main() {new Thread (Go).Start(); // Call Go() on a new threadGo(); // Call Go() on the main thread}static void Go() {// Declare and use a local variable - ‘cycles‘for (int cycles = 0; cycles < 5; cycles++) Console.Write (‘?‘);}

執行結果:

??????????

每個線程的記憶體棧裡都建立了一個單獨的變數cycle,所以輸出是10個?
如果是引用同一個對象的話,線程則共用這個資料:

按 Ctrl+C 複製代碼按 Ctrl+C 複製代碼

因為兩個線程都調用Go(),它們共用done這個變數,所以done只輸出一次:

Done

static變數提供一種不同的方式線上程中共用變數,這裡是一個例子:

class ThreadTest {static bool done; // Static fields are shared between all threadsstatic void Main() {new Thread (Go).Start();Go();}static void Go() {if (!done) { done = true; Console.WriteLine ("Done"); }}}

這裡輸出就不太確定了,看起來要輸出兩次done,其實不可能。我們交換一下Go的次序,

class ThreadTest {static bool done; // Static fields are shared between all threadsstatic void Main() {new Thread (Go).Start();Go();}static void Go() {if (!done) { Console.WriteLine ("Done"); done = true; }}}

done就有可能輸出兩次。因為在一個線程計算if運算式然後執行Console.WriteLine ("Done");的時候,另一個線程可能有機會在done的值改變之前先輸出done。
其實在C#中可以用lock來達到這個目的:

class ThreadSafe {static bool done;static object locker = new object();static void Main() {new Thread (Go).Start();Go();}static void Go() {lock (locker) {if (!done) { Console.WriteLine ("Done"); done = true; }}}}

當兩個線程同時競爭一個鎖時,一個線程等待,或者說阻塞,直到鎖空出來。這主要是保證同時只能有一個線程可以進入臨界代碼地區,"Done"只會被輸出一次。
代碼是以這樣的方式被保護的,來自於多線程內容相關的不確定性,叫做安全執行緒。
臨時地暫停,或阻塞,是線程同步的準系統。
如果一個線程想要暫停,或者休眠一段時間,可以用:

Thread.Sleep (TimeSpan.FromSeconds (30)); // 阻塞30秒

一個線程可以通過調用Join等待另一個線程結束:

Thread t = new Thread (Go); // 假設Go是靜態函數。t.Start();Thread.Join (t); // 阻塞,只到線程t結束。

線程如何工作:
在內部,多線程是被線程調度器管理的,是CLR代替作業系統乾的活。線程調度器要保證所有活躍線程合理分配執行時間,以及在等待中的線程(這些線程是不消耗CPU時間的)。
在單核機器上,線程調度是以在活躍線程間快速切換時間片的方式工作的。就像就第一個例子,重複輸出x或y的線程輪換得到時間片。在Windows XP下,一個時間片就是幾十毫秒,這還是要比CPU線上程間切換能幹更多事,一次線程切換也就幾毫秒的事。
在多核機器上,多線程的實現是結合了時間片輪換和並發,並發是不同的線程同時運行在不同的CPU上,因為機器要啟動並執行線程數遠遠大於CPU的數量,所以還需要時間片切換。
線程不能控制自己什麼時候執行,完全由作業系統的時間片切換機制來控制。

線程和進程:

總是有面試官喜歡把線程和進程做比較,其實兩者根本不是一個層級的東西。一個單獨的應用程式內所有的線程都在邏輯或屬於一個進程的。進程:一個運行應用程式的作業系統單元。線程與進程有些相似之處,比如:對於執行個體,進程和線程都是典型的時間片輪換的執行機制。關鍵的不同點在於進程間是相互獨立的,而同一個應用程式裡的線程間是共用堆記憶體的,這也是效能的用武之地:一個線程可以在後台運行,另一個線程可以顯示得到的資料。

什麼時候應該用多線程:

  • 一個普通的多線程程式在後台運行耗時的任務時。主線程保持運行狀態,背景工作執行緒幹背景活。在Windows Form程式裡,如果主線程被長時間佔用,鍵盤和滑鼠的操作就不能處理了,然後程式就變成“無響應”了。所以,需要把耗時的任務放在後台運行,讓主線程保證響應使用者輸入。
  • 在非UI程式中,比如Windows服務,多線程就特別有用了,當等待另一台機器(例如一個應用伺服器,資料庫伺服器,用戶端)的響應時,用一個背景工作執行緒來等待,讓主線程保持暢通。
  • 多線程的另一個用處是在函數中有大量計算時,函數劃成多個線程可以在多核的機器上執行更快(可以用Environment.ProcessorCount得到CPU核心數量)。
  • 一個C#程式可以通過兩種方式成為多線程:顯示地建立線程,或者使用.NET顯示建立線程的功能(比如BackgroundWorker,線程池,定時器,遠程伺服器,WebSerivce或ASP.NET程式),在後面這些情況下,只能是多線程。單線程的web伺服器肯定不行。在無狀態的web伺服器裡,多線程是相當簡單的。主要的問題是如果處理快取資料的鎖機制。

什麼時候不應該用多線程:

多線程也有缺點,最大的問題是會讓程式變得複雜,多線程本身並不複雜,複雜在於線程間的互動。能讓開發週期變長,以及Bug變多。所以需要把多個線程間的互動設計的盡量簡單,或者就別用多線程,除非你可以保證的很好。

過多地線上程間切換和分配記憶體棧,也會帶來CPU資源的消耗,通常,當硬碟IO很多時,只有一兩個線程依次執行任務的程式效能要更好,而多個效能同時執行一個任務的效能不怎麼樣。後面會討論生產者/消費者模型。

建立和啟動線程:

 可以用Thread類的建構函式建立線程,傳遞一個ThreadStart的代理作為參數,這個代理指向將要執行的函數,以下是這個代理的定義:

public delegate void ThreadStart();

執行Start()函數,線程即開始運行,在函數結束後線程會返回,下面是建立ThreadStart的C#文法:

按 Ctrl+C 複製代碼按 Ctrl+C 複製代碼

線程t執行Go函數,同時主線程也調用Go,執行結果是:

hello!hello!

也可以用C#的文法糖:編譯器會自動建立一個ThreadStart的代理。

static void Main() {Thread t = new Thread (Go); // No need to explicitly use ThreadStartt.Start();...}static void Go() { ... }

還有更簡單的匿名函數文法:

static void Main() {Thread t = new Thread (delegate() { Console.WriteLine ("Hello!"); });t.Start();}

給ThreadStart傳遞參數:這種形式只能傳遞一個參數

public delegate void ParameterizedThreadStart (object obj);
class ThreadTest {static void Main() {Thread t = new Thread (Go);t.Start (true); // == Go (true)Go (false);}static void Go (object upperCase) {bool upper = (bool) upperCase;Console.WriteLine (upper ? "HELLO!" : "hello!");}

結果:

hello!HELLO!

如果用匿名函數方式:可以傳遞多個參數,且也不需要類型轉換,

static void Main() {Thread t = new Thread (delegate() { WriteText ("Hello"); });t.Start();}static void WriteText (string text) { Console.WriteLine (text); }

還有一種傳參的方式是傳一個執行個體過去,而不是傳一個靜態函數:

class ThreadTest {bool upper;static void Main() {ThreadTest instance1 = new ThreadTest();instance1.upper = true;Thread t = new Thread (instance1.Go);t.Start();ThreadTest instance2 = new ThreadTest();instance2.Go(); // Main thread – runs with upper=false}void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!"); }

線程命名:線程有一個Name屬性,在調試時很有用。

class ThreadNaming {static void Main() {Thread.CurrentThread.Name = "main";Thread worker = new Thread (Go);worker.Name = "worker";worker.Start();Go();}static void Go() {Console.WriteLine ("Hello from " + Thread.CurrentThread.Name);}}

輸出:

Hello from mainHello from worker

前台線程和後台線程:

預設情況下,線程都是前台線程,意味著任何一個前台線程正在運行,程式就是啟動並執行。而後台線程在所有前台線程終止時也會立即終止。

把線程從前台改為後台,線程在CPU調度器的優先順序和狀態是不會改變的。

class PriorityTest {static void Main (string[] args) {Thread worker = new Thread (delegate() { Console.ReadLine(); });if (args.Length > 0) 
worker.IsBackground = true;worker.Start();}}

如果這個程式執行時不帶參數,worker線程預設是前台線程,並且會在ReadLine這一行等著使用者輸入。同時,主線程退出,但是程式會繼續運行,因為ReadLine也是前台線程。如果傳了一個參數給Main函數,worker線程的狀態則被設定成後台狀態,程式幾乎會在主線程結束時立即退出--終於ReadLine。當後台線程以這種方式終止時,任何代碼都不再執行了,這種代碼是不推薦的,所以最好在程式退出前等待所有後台線程,可以用逾時時間(Thread.Join)來做。如果因為某些原因worker線程一直不結束,也能終止這個線程,這種情況下最好記錄一下日誌來分析什麼情況導致的。

在Windows Form中被拋棄的前台線程是個潛在的危險,因為程式在主線程結束將要退出時,它還在繼續運行。在Windows的工作管理員裡,它在應用程式Tab裡會消失,但在進程Tab裡還在。除非使用者顯式地結束它。

常見的程式退出失敗的可能性就是忘記了前台線程。

線程的優先順序:線程的優先順序決定了線程的執行時間。

enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }

線程的優先順序為Highest時,並不意味著線程會即時運行,要想即時運行,進程的優先順序也得是High。當你的進程的優先順序是High時,如果程式進入了死迴圈,系統會死結。這個時候就只有按電源鍵了。所以,慎用。

最好將即時線程和UI分開在兩個線程,並設定成不同的優先順序,通過遠程或共用記憶體通訊,共用記憶體需要P/Invoking Win32 API(CreateFileMapping和MapViewOfFile)。

線程的異常處理:線程一旦啟動,任何在try/catch/finally範圍內建立線程的代碼塊與try/catch/finally就沒有什麼關係了。

public static void Main() {try {new Thread (Go).Start();}catch (Exception ex) {// We‘ll never get here!Console.WriteLine ("Exception!");}static void Go() { throw null; }}

上例中的try/catch基本沒用了,新建立的線程可能是未處理的Null 參考異常,最好線上程要執行的代碼裡加異常捕獲:

public static void Main() {new Thread (Go).Start();}static void Go() {try {...throw null; // this exception will get caught below...}catch (Exception ex) {Typically log the exception, and/or signal another threadthat we‘ve come unstuck...}

從.NET2.0開始,線程上任何未處理的異常會導致整個程式掛掉,意味著千萬別忽略異常,線上程要執行的函數裡,給每個可能異常的代碼加上try/catch。這可能有點麻煩,所以,很多人這樣處理,用全域的異常處理:

using System;using System.Threading;using System.Windows.Forms;static class Program {static void Main() {Application.ThreadException += HandleError;Application.Run (new MainForm());}static void HandleError (object sender, ThreadExceptionEventArgs e) {Log exception, then either exit the app or continue...}}

Application.ThreadException事件會在代碼拋出異常時被觸發,這樣看起來很完美--可以捕獲所有異常,但在worker線程上的異常可能捕獲不了,在main函數裡的表單的建構函式,在Windows的訊息迴圈之前就執行了。.NET提供了一個低層的事件捕獲全域異常:AppDomain.UnhandledException,它才可以捕獲所有異常(UI和非UI的)。雖然它提供了一個很好的方式捕獲所有異常並記錄異常日誌,但是它沒有辦法阻止程式關係,也沒有辦法阻止.NET的異常對話方塊

C# 線程入門 00

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.