C#的執行緒(一)
初識執行緒
執行緒是一個獨立的執行單元,每個程式內部都有多個執行緒,每個執行緒都可以各自同時執行指令。每個執行緒都有自己獨立的棧,但是與程式內的其他執行緒共享記憶體。但是對於.NET的客戶端程式(Console,WPF,WinForms)是由CLR建立的單執行緒(主執行緒,且只建立一個執行緒)來啟動。在該執行緒上可以建立其他執行緒。
圖:
執行緒工作方式
多執行緒由內部執行緒排程程式管理,執行緒排程器通常是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”。
圖:
執行緒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在不同的執行緒棧上,所以相互獨立不受影響。
圖:
如果不同執行緒指向同一個例項的引用,那麼不同的執行緒共享該例項。
如:
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);
}
委託非同步
委託非同步可以返回任意型別個數的值。
使用委託非同步的方式:
- 宣告一個和方法匹配的委託
- 呼叫該委託的BeginInvoke方法,獲取返回型別為IAsyncResult的值
- 呼叫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做了三件事情:
- 等待委託非同步的結束。
- 獲取返回值。
- 丟擲未處理異常給呼叫執行緒。
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);
}
參考: