行程
行程是一個執行中的程式, 現行分時系統中, 行程是基本的工作單元. 行程包括程式碼(本文區)、程式計數器數值、暫存器內容、行程堆疊、堆積、變數的資料區間。
行程有五種狀態 : 新產生、執行、等待、就緒、結束, 任何時候只能一個行程在執行狀態, 但有許多行程在等待和就緒的狀態。
每一個行程在作業系統中都對應一個行程控制表(PCB), PCB記載所代表的行程相關資訊。
分時系統可將CPU在不同行程間不斷切換, 以便讓使用者可以在自己的行程執行時與它互動。
一個新的行程最初置於就緒佇列中, 此行程被CPU執行時, 可觸發如下事件 :
發出I/O要求,然後置於一個I/O佇列中。
產生一個子行程,並等待其結束。
強行移離CPU(和中斷的結果一樣),然後放回就緒佇列中。
執行緒
執行緒是CPU使用時的一個基本單位, 由執行緒ID、程式計數器、一組暫存器及一個堆疊空間組成。
一個行程可擁有多個執行緒, 每個執行緒共用行程的資源(Ram, File, Signal)
行程的產生對系統來說是重擔,如果新行程和現存行程執行相同的工作,比較有效率的做法是讓一個行程產生新的執行緒來達到相同的目的。
多執行緒程式好處如下 :
應答 : 如一個執行緒載入影像時, 另一執行緒可以跟使用者互動
資源共享 : 執行緒共享行程內的Ram, 但也容易造成Dead Lock
經濟 : 執行緒消耗的資源比行程少
正規寫法
正規啟動執行緒, 必需使用 new Thread(ThreadStart物件).Start();
麻煩難懂的地方是, 什麼是ThreadStart物件.
我們可以把ThreadStart想成是一個物件, 把要作的任務塞在裏面. 所以更進一步的完整代碼如下
ThreadStart s=new ThreadStart(某個方法);
new Thread(s).Start();
現在怪異的地方又來了, 把某個方法塞到ThreadStart()這個建構子. 方法之內不是都只是塞資料型態嗎, 怎麼又塞方法呢?
原來他是使用Delegate(委派)的方式, ThreadStart是系統就寫好的, 原始碼如下
public delegate void ThreadStart();
我們只要產生ThreadStart物件, 將方法塞到裏面, 這個方法就會包在物件裏面, 然後這個方法就可以供Thread使用了。
using System; using System.Threading; namespace ConsoleApp1 { class Program { static void Main(string[] args) { ThreadStart s = new ThreadStart(task); Thread t = new Thread(s); t.Name = "New Thread 0"; t.Start(); Console.WriteLine("Main 結束了"); } static void task(){ for (int i = 0; i < 20; i++) { Thread.Sleep(100); Console.WriteLine("{0} : {1}", Thread.CurrentThread.Name, i); } } } }
請注意一下, Main主執行緒結束後, 新的執行緒還是一直在背景努力的執行喔
Simple Thread
將任務包含在一個方法內, 再建立新執行緒, 然後把任務傳入, 最後使用Start()啟動
但為何可以直接使用 new thread(task).Start()呢, 因為編譯器會自動啟動如下委派
public delegate void ThreadStart();
static void Main(string[] args) { new Thread(task).Start(); for(int i = 0; i < 1000; i++) { Thread t = Thread.CurrentThread; Console.WriteLine("Thread_{0}:{1}", t.ManagedThreadId, i); } } static void task() { for(int i = 0; i < 1000; i++) { Thread t = Thread.CurrentThread; Console.WriteLine("Thread_{0}:{1}", t.ManagedThreadId, i); } }
執行結果如下, 主執行緒及新執行緒交互執行, 完全無法預測
Thread_1:8 Thread_1:9 Thread_3:0 Thread_3:1 Thread_3:2 Thread_3:3 Thread_3:4 Thread_3:5 Thread_3:6 Thread_3:7 Thread_3:8 Thread_3:9 Thread_3:10 Thread_3:11 Thread_3:12 Thread_3:13 Thread_3:14 Thread_3:15 Thread_3:16 Thread_3:17 Thread_3:18 Thread_3:19 Thread_3:20 Thread_3:21 Thread_3:22 Thread_3:23 Thread_3:24 Thread_1:10 Thread_1:11 Thread_1:12 Thread_1:13 Thread_1:14
ParameterizedThreadStart
若要將參數傳入任務方法, 可使用如下程式碼
static void Main(string[] args)
{
new Thread(task).Start(10);
for (int i = 0; i < 100; i++)
{
Thread t = Thread.CurrentThread;
Console.WriteLine("thread_{0}:{1}", t.ManagedThreadId, i);
}
}
static void task(object number)
{
for (int i = 0; i < (int)number; i++)
{
Thread t = Thread.CurrentThread;
Console.WriteLine("thread_{0}:{1}", t.ManagedThreadId, i);
}
}
上述紅色部份, 將參數放入Start(10)方法之內, 此時系統自動啟動
public delegate void ParameterizedThreadStart(Object obj);
所以也可以寫成如下
new Thread(new ParameterizedThreadStart(task)).Start(10);
等待
如果希望執行緒完成後, 才開始調用者的流程, 可以使用Thread物件的Join()方法, 如下程式碼
class Program { static void Main(string[] args) { Thread t1 = new Thread(task); Thread t2 = new Thread(task); Thread t3 = new Thread(task); t1.Start("a"); t2.Start("b"); t3.Start("c"); t1.Join(); t2.Join(); t3.Join(); for (int i = 0; i < 1000; i++) { Console.Write("+"); } } static void task(object param) { for(int i = 0; i < 1000; i++) { Console.Write(param); } } }
上述程式碼, 主執行緒會停止, 等待t1, t2, t3三個執行緒完成後, 才會開始執行主執行緒. 而在主執行緒停止期間, t1, t2, t3處於同室操戈的狀態, 誰先誰後, 只有神知道
執行結果如下
aaaaaaaaaaaaaaaaaaaaaaaaaaaacccccccbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ThreadPool
養一堆執行緒等待工作的到來, 工作一來, 都放在Queue裏, 再由執行緒集區一個一個消化掉
ThreadPool.QueueUserWorkItem(WaitCallback)
將工作排入執行緒佇列中,由 ThreadPool 對其做管理
WaitCallback
為要執行的方法, 需先產生WaitCallback物件, 並將方法傳入建構子之中, 如下之代碼 new WaitCallback(Doworker);
static void Main(string[] args) { for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(Doworker)); } Console.WriteLine("Main thread exit"); Console.ReadKey(); } static void Doworker(object number) { Thread t = Thread.CurrentThread; Console.WriteLine("No.{0}-Thread[{1}]:{2}", number, t.ManagedThreadId, t.ThreadState); Thread.Sleep(1000); } /* 以上可簡化成 ThreadPool.QueueUserWorkItem(callback => { Thread t = Thread.CurrentThread; Console.WriteLine("No.{0}-Thread[{1}]:{2}", number, t.ManagedThreadId, t.ThreadState); Thread.Sleep(1000); }); */
執行結果如下
Main thread exit No.-Thread[7]:Background No.-Thread[5]:Background No.-Thread[3]:Background No.-Thread[8]:Background No.-Thread[4]:Background No.-Thread[6]:Background No.-Thread[10]:Background No.-Thread[9]:Background No.-Thread[11]:Background No.-Thread[3]:Background .請按任意鍵繼續 . . .
以上代碼, 主執行緒交付了10個任務, 由執行緒集區執行, 集區啟動10條執行緒工作. 請注意, 主執行緒一結束, 就算集區還沒完成, 集區還是跟著毀滅. 所以在主執行緒要加入ReadKey()等待使用者按任何鍵.
Lambda
執行緒也可以使用 Lambda簡易寫法, 如下
new Thread(()=>{ //要執行的任務 }).Start()
跨線程
在C#中, 若要更新UI畫面時, 只能使用UI主執行緒進行更新. 也就是說, 新開啟的執行緒是無法操作UI控制項.
那麼, 在新的執行緒執行到某一時間點, 想要更改控制項的話, 那該怎麼辦呢! 這時就必需由新執行緒跨到UI主執行緒, 把更新的工作交給UI主執行緒去執行. 此時稱為跨線程.
事實上, 跨線程要求UI主執行緒更新畫面時, 並不是馬上就執行. 必需等到UI主執行緒有空閒的時候,才會去處理畫面的事。
BackgroundWorker
BackgroundWorker這名字太長了, 所以我們就簡稱為bg
UI的變更, 只能由UI主執行緒更改, 其他新生的執行緒無法更改UI, 否則會發生Exception. 此設計與Android 一模一樣.
在背景執行的執行緒, 若想控制UI, 必需傳回相關資料給UI主執行緒, 待UI主執行緒有空時才進行更改. 最常用也是最方便的就是bg物件.
BackgroundWorker 可以指定如下委派
DoWork : 背景要執行的任務, 由DoWorkEventHandler處理
ProgressChanged : 進度發生變化時會被執行的方法, 此方法是由UI主執行緒執行, 所以可以在此控制 一般會在DoWork中下達bg.ReportProgress(int) 來觸發此方法
RunWorkerCompleted : 執行緒結束後會被執行的方法, 也是由UI主執行緒控制
在DoWork裏, 若不小心操作了UI的控制項, 整個執行緒將立即中斷Crash並跳出, 但不會出現任何的錯誤訊息及Exception, 此Issue非常難除錯, 請注意
如下代碼, 用於顯示進度條, 請注意, CancelAsync()並不能真正的停止Thread
public partial class MainWindow : Window { BackgroundWorker bg; bool stopFlag = false; public MainWindow() { InitializeComponent(); } private void btnOk_Click(object sender, RoutedEventArgs e) { bg = new BackgroundWorker(); //背景執行的任務 bg.DoWork += new DoWorkEventHandler(bgDoWork); //進度變化時, 由背景任務的ReportProgress(i)觸發 bg.ProgressChanged += new ProgressChangedEventHandler(bgProgressChanged); //任務完成時要執行的工作 bg.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgRunWorkerCompleted); bg.WorkerReportsProgress = true; //允許CancelAsync(), 但此指令是無法中止執行緒的, 日後可能會被拿掉 //所以, 底下加了也是白加的 bg.WorkerSupportsCancellation = true; //開始執行執行緒 bg.RunWorkerAsync(); } private void btnCancel_Click(object sender, RoutedEventArgs e) { stopFlag = true; } private void bgDoWork(object sender, DoWorkEventArgs e) { for(int i = 0; i <= 100; i++) { bg.ReportProgress(i); Thread.Sleep(50); if (stopFlag) { //bg.CancelAsync();沒啥用處 break; } } } private void bgProgressChanged(object sender, ProgressChangedEventArgs e) { psBar.Value = e.ProgressPercentage; } private void bgRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { MessageBox.Show("complete"); } }
執行結果如下
最後要注意一件事, BackgroundWorker雖是很直覺化的設計, 但如果使用多個bg物件, 會造成阻塞. 也就是說, 明明就下達了
bg.RunWorkerAsync();
但就是要等很久才會執行. 這可能是VS設計上的bug, 所以本人最初的專案都使用bg解決UI與Thread的問題. 到了後期, 全改為下面的方式了.
Dispatcher
dispatcher是調度員的意思. VS使用這個類別, 來分配時間及通知UI主執行緒執行UI更新的任務. dispatcher只適用在WPF. 如果是Windows Form, 必需使用Invoke. 在此有個建議, Windows Form早就該拋棄了, 而且Invoke也被列入即將棄用的項目.
Windows Form
在Windows Form之, 要執到跨線程的工作, 可以使用Windows Form的物件方法Invoke(delegate, parameter), 或是InvokeBegin().
WPF
但在WPF中, 似乎沒有Invoke這個方法了. 此時就必需使用Dispatcher, 如下
Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => { //UI控製元件代碼寫在這裏 }));