C#執行緒

JoeSnail發表於2017-02-23

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, null);
  //
  // ... 此時可以同步處理其他事情
  //
  int result = method.EndInvoke(cookie);
  Console.WriteLine("String length is: " + result);        

  int Work(string s) { return s.Length; }

使用回撥函式獲取返回值

static void Main()
{
  Func<string, int> method = Work;
  method.BeginInvoke ("test", Done, 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);
}

參考:

相關文章