1. 程式人生 > 程式設計 >C# 執行緒相關知識總結

C# 執行緒相關知識總結

初識執行緒

執行緒是一個獨立的執行單元,每個程序內部都有多個執行緒,每個執行緒都可以各自同時執行指令。每個執行緒都有自己獨立的棧,但是與程序內的其他執行緒共享記憶體。但是對於.NET的客戶端程式(Console,WPF,WinForms)是由CLR建立的單執行緒(主執行緒,且只建立一個執行緒)來啟動。在該執行緒上可以建立其他執行緒。

圖:

C# 執行緒相關知識總結

執行緒工作方式

多執行緒由內部執行緒排程程式管理,執行緒排程器通常是CLR委派給作業系統的函式。執行緒排程程式確保所有活動執行緒都被分配到合適的執行時間,執行緒在等待或阻止時 (例如,在一個獨佔鎖或使用者輸入) 不會消耗 CPU 時間。
在單處理器計算機上,執行緒排程程式是執行時間切片 — 迅速切換每個活動執行緒。在 Windows 中,一個時間片是通常數十毫秒為單位的區域 — — 相比來說 執行緒間相互切換比CPU更消耗資源。在多處理器計算機上,多執行緒用一種混合的時間切片和真正的併發性來實現,不同的執行緒會在不同的cpu執行程式碼。

建立執行緒

如:

using System;
using System.Threading;

class ThreadTest
{
 static void Main()
 {
  Thread t = new Thread (Write2);     // 建立執行緒t
  t.Start();                // 執行 Write2()
 
  // 同時執行主執行緒上的該方法
  for (int i = 0; i < 1000; i++) Console.Write ("1");
 }
 
 static void Write2()
 {
  for (int i = 0; i < 1000; i++) Console.Write ("2");
 }
}

輸出

111122221122221212122221212......

在主執行緒上建立了一個新的執行緒,該新執行緒執行WrWrite2方法,在呼叫t.Start()時,主執行緒並行,輸出“1”。

圖:

C# 執行緒相關知識總結

執行緒Start()之後,執行緒的IsAlive屬性就為true,直到該執行緒結束(當執行緒傳入的方法結束時,該執行緒就結束)。

CLR使每個執行緒都有自己獨立的記憶體棧,所以每個執行緒的本地變數都相互獨立。

如:

static void Main() 
{
 new Thread (Go).Start();   // 建立一個新執行緒,並呼叫Go方法
 Go();             // 在主執行緒上呼叫Go方法
}
 
static void Go()
{
 // 宣告一個本地區域性變數 cycles
 for (int cycles = 0; cycles < 5; cycles++) Console.Write ('N');
}

輸出

NNNNNNNNNN (共輸出10個N)

在新執行緒和主執行緒上呼叫Go方法時分別建立了變數cycles,這時cycles在不同的執行緒棧上,所以相互獨立不受影響。

圖:

C# 執行緒相關知識總結

如果不同執行緒指向同一個例項的引用,那麼不同的執行緒共享該例項。

如:

class ThreadTest
{
 //全域性變數
 int i;
 
 static void Main()
 {
  ThreadTest tt = new ThreadTest();  // 建立一個ThreadTest類的例項
  new Thread (tt.Go).Start();
  tt.Go();
 }
 
 // Go方法屬於ThreadTest的例項
 void Go() 
 {
   if (i==1) { ++i; Console.WriteLine (i); }
 }
}

輸出

2

新執行緒和主執行緒上呼叫了同一個例項的Go方法,所以變數i共享。

靜態變數也可以被多執行緒共享

class ThreadTest 
{
 static int i;  // 靜態變數可以被執行緒共享
 
 static void Main()
 {
  new Thread (Go).Start();
  Go();
 }
 
 static void Go()
 {
  if (i==1) { ++i; Console.WriteLine (i); }
 }
}

輸出

2

如果將Go方法的程式碼位置互換

 static void Go()
 {
  if (i==1) { Console.WriteLine (i);++i;}
 }

輸出

1 1(有時輸出一個,有時輸出兩個)

如果新執行緒在Write之後,done=true之前,主執行緒也執行到了write那麼就會有兩個done。

不同執行緒在讀寫共享欄位時會出現不可控的輸出,這就是多執行緒的執行緒安全問題。

解決方法: 使用排它鎖來解決這個問題--lock

class ThreadSafe 
{
 static bool done;
 static readonly object locker = new object();
 
 static void Main()
 {
  new Thread (Go).Start();
  Go();
 }
 
 static void Go()
 {
  //使用lock,確保一次只有一個執行緒執行該程式碼
  lock (locker)
  {
   if (!done) { Console.WriteLine ("Done"); done = true; }
  }
 }
}

當多個執行緒都在爭取這個排它鎖時,一個執行緒獲取該鎖,其他執行緒會處於blocked狀態(該狀態時不消耗cpu),等待另一個執行緒釋放鎖時,捕獲該鎖。這就保證了一次
只有一個執行緒執行該程式碼。

Join和Sleep

Join可以實現暫停另一個執行緒,直到呼叫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");
}

輸出

yyyyyy..... Thread t has ended!

執行緒t呼叫Join方法,阻塞主執行緒,直到t執行緒執行結束,再執行主執行緒。

Sleep:暫停該執行緒一段時間

Thread.Sleep (TimeSpan.FromHours (1)); // 暫停一個小時
Thread.Sleep (500);           // 暫停500毫秒

Join是暫停別的執行緒,Sleep是暫停自己執行緒。

上面的例子是使用Thread類的建構函式,給建構函式傳入一個ThreadStart委託。來實現的。

public delegate void ThreadStart();

然後呼叫Start方法,來執行該執行緒。委託執行完該執行緒也結束。

如:

class ThreadTest
{
 static void Main() 
 {
  Thread t = new Thread (new ThreadStart (Go));
 
  t.Start();  // 執行Go方法
  Go();    // 同時在主執行緒上執行Go方法
 }
 
 static void Go()
 {
  Console.WriteLine ("hello!");
 }
}

多數情況下,可以不用new ThreadStart委託。直接在建構函式裡傳入void型別的方法。

Thread t = new Thread (Go); 

使用lambda表示式

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

Foreground執行緒和Background執行緒

預設情況下建立的執行緒都是Foreground,只要有一個Foregournd執行緒在執行,應用程式就不會關閉。
Background執行緒則不是。一旦Foreground執行緒執行完,應用程式結束,background就會強制結束。
可以用IsBackground來檢視該執行緒是什麼型別的執行緒。

執行緒異常捕獲

public static void Main()
{
 try
 {
  new Thread (Go).Start();
 }
 catch (Exception ex)
 {
  // 不能捕獲異常
  Console.WriteLine ("Exception!");
 }
}
 
static void Go() { throw null; }  //丟擲 Null異常

此時並不能在Main方法裡捕獲執行緒Go方法的異常,如果是Thread自身的異常可以捕獲。

正確捕獲方式:

public static void Main()
{
  new Thread (Go).Start();
}
 
static void Go()
{
 try
 {
  // ...
  throw null;  // 這個異常會被下面捕獲
  // ...
 }
 catch (Exception ex)
 {
   // ...
 }
}

執行緒池

當建立一個執行緒時,就會消耗幾百毫秒cpu,建立一些新的私有區域性變數棧。每個執行緒還消耗(預設)約1 MB的記憶體。執行緒池通過共享和回收執行緒,允許在不影響效能的情況下啟用多執行緒。
每個.NET程式都有一個執行緒池,執行緒池維護著一定數量的工作執行緒,這些執行緒等待著執行分配下來的任務。

執行緒池執行緒注意點:

1 執行緒池的執行緒不能設定名字(導致執行緒除錯困難)。

2 執行緒池的執行緒都是background執行緒

3 阻塞一個執行緒池的執行緒,會導致延遲。

4 可以隨意設定執行緒池的優先順序,在回到執行緒池時改執行緒就會被重置。
通過Thread.CurrentThread.IsThreadPoolThread.可以檢視該執行緒是否是執行緒池的執行緒。

使用執行緒池建立執行緒的方法:

  • Task
  • ThreadPool.QueueUserWorkItem
  • Asynchronous delegates
  • BackgroundWorker

TPL

Framework4.0下可以使用Task來建立執行緒池執行緒。呼叫Task.Factory.StartNew(),傳遞一個委託

  • Task.Factory.StartNew
  • static void Main() 
    {
     Task.Factory.StartNew (Go);
    }
     
    static void Go()
    {
     Console.WriteLine ("Hello from the thread pool!");
    }

Task.Factory.StartNew 返回一個Task物件。可以呼叫該Task物件的Wait來等待該執行緒結束,呼叫Wait時會阻塞呼叫者的執行緒。

  • Task建構函式 給Task建構函式傳遞Action委託,或對應的方法,呼叫start方法,啟動任務
  • static void Main() 
    {
     Task t=new Task(Go);
     t.Start();
    }
     
    static void Go()
    {
     Console.WriteLine ("Hello from the thread pool!");
    }
  • Task.Run 直接呼叫Task.Run傳入方法,執行。
  • static void Main() 
    {
     Task.Run(() => Go());
    }
     
    static void Go()
    {
     Console.WriteLine ("Hello from the thread pool!");
    }

QueueUserWorkItem

QueueUserWorkItem沒有返回值。使用 QueueUserWorkItem,只需傳遞相應委託的方法就行。

static void Main()
{
 //Go方法的引數data此時為空
 ThreadPool.QueueUserWorkItem (Go);
 //Go方法的引數data此時為123
 ThreadPool.QueueUserWorkItem (Go,123);
 Console.ReadLine();
}
 
static void Go (object data) 
{
 Console.WriteLine ("Hello from the thread pool! " + data);
}

委託非同步

委託非同步可以返回任意型別個數的值。
使用委託非同步的方式:

  1. 宣告一個和方法匹配的委託
  2. 呼叫該委託的BeginInvoke方法,獲取返回型別為IAsyncResult的值
  3. 呼叫EndInvoke方法傳遞IAsyncResulte型別的值獲取最終結果

如:

static void Main()
{
 Func<string,int> method = Work;
 IAsyncResult cookie = method.BeginInvoke ("test",null,null);
 //
 // ... 此時可以同步處理其他事情
 //
 int result = method.EndInvoke (cookie);
 Console.WriteLine ("String length is: " + result);
}
 
static int Work (string s) { return s.Length; }

使用回撥函式來簡化委託的非同步呼叫,回撥函式引數為IAsyncResult型別

static void Main()
{
 Func<string,int> method = Work;
 method.BeginInvoke ("test",Done,method);
 // ...
 //並行其他事情
}
 
static int Work (string s) { return s.Length; }
 
static void Done (IAsyncResult cookie)
{
 var target = (Func<string,int>) cookie.AsyncState;
 int result = target.EndInvoke (cookie);
 Console.WriteLine ("String length is: " + result);
}

使用匿名方法

Func<string,int> f = s => { return s.Length; };
 f.BeginInvoke("hello",arg =>
 {
   var target = (Func<string,int>)arg.AsyncState;
   int result = target.EndInvoke(arg);
   Console.WriteLine("String length is: " + result);
 },f);

執行緒傳參和執行緒返回值

Thread

Thread建構函式傳遞方法有兩種方式:

public delegate void ThreadStart();
public delegate void ParameterizedThreadStart (object obj);

所以Thread可以傳遞零個或一個引數,但是沒有返回值。

  • 使用lambda表示式直接傳入引數。
  • static void Main()
    {
     Thread t = new Thread ( () => Print ("Hello from t!") );
     t.Start();
    }
     
    static void Print (string message) 
    {
     Console.WriteLine (message);
    }
  • 呼叫Start方法時傳入引數
  • static void Main()
    {
     Thread t = new Thread (Print);
     t.Start ("Hello from t!");
    }
     
    static void Print (object messageObj)
    {
     string message = (string) messageObj;  
     Console.WriteLine (message);
    }

Lambda簡潔高效,但是在捕獲變數的時候要注意,捕獲的變數是否共享。

如:

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

輸出

0223447899

因為每次迴圈中的i都是同一個i,是共享變數,在輸出的過程中,i的值會發生變化。

解決方法-區域性域變數

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

這時每個執行緒都指向新的域變數temp(此時每個執行緒都有屬於自己的花括號的域變數)在該執行緒中temp不受其他執行緒影響。

委託

委託可以有任意個傳入和輸出引數。以Action,Func來舉例。

  • Action 有零個或多個傳入引數,但是沒有返回值。
  • Func 有零個或多個傳入引數,和一個返回值。
  •  Func<string,int> method = Work;
     IAsyncResult cookie = method.BeginInvoke("test",null);
     //
     // ... 此時可以同步處理其他事情
     //
     int result = method.EndInvoke(cookie);
     Console.WriteLine("String length is: " + result);    
    
     int Work(string s) { return s.Length; }

使用回撥函式獲取返回值

static void Main()
{
 Func<string,null);
 // ...
 //並行其他事情
}
 
static int Work (string s) { return s.Length; }
 
static void Done (IAsyncResult cookie)
{
 var target = (Func<string,int>) cookie.AsyncState;
 int result = target.EndInvoke (cookie);
 Console.WriteLine ("String length is: " + result);
}

EndInvoke做了三件事情:

  1. 等待委託非同步的結束。
  2. 獲取返回值。
  3. 丟擲未處理異常給呼叫執行緒。

Task

Task泛型允許有返回值。

如:

static void Main()
{
 // 建立Task並執行
 Task<string> task = Task.Factory.StartNew<string>
  ( () => DownloadString ("http://www.baidu.com") ); 
 // 同時執行其他方法
 Console.WriteLine("begin");
 //等待獲取返回值,並且不會阻塞主執行緒
 Console.WriteLine(task.Result);
 Console.WriteLine("end");
} 
static string DownloadString (string uri)
{
 using (var wc = new System.Net.WebClient())
  return wc.DownloadString (uri);
}

參考:

http://www.albahari.com/threading/

以上就是C# 執行緒相關知識總結的詳細內容,更多關於C# 執行緒的資料請關注我們其它相關文章!