委派與執行緒 建國科技大學 資管系 饒瑞佶
Delegate委派機制 Delegate是一個容器,裡面存放著需要被呼叫的方 法參考,要使用委派的話一般會搭配事件(Event)與 Invoke()方法,我們每點擊一個按鈕的背後其實都 是交由委派去決定要執行哪一個方法 委派可以看成是方法的指標,利用委派可以間接叫 用方法 委派是.NET中事件呼叫與執行的基礎
.NET中透過委派執行事件 圖片來源:https://dotblogs.com.tw/joysdw12/2013/06/21/delegate-winfom
實際開啟 *.Designer.cs 檔案就能夠發現 Click 事件都是透過 EventHandler 委派來執行 this.button1.Location = new System.Drawing.Point(118, 12); this.button1.Name = "button1"; this.button1.Size = new System.Drawing.Size(133, 23); this.button1.TabIndex = 0; this.button1.Text = "OpenFormRetrunValue"; this.button1.UseVisualStyleBackColor = true; this.button1.Click += new System.EventHandler(this.button1_Click); 事件驅動(大家一般稱呼),但背後是委派機制 這個委派是.NET已經幫我們建立的
委派對應事件的指定與刪除 在 .NET 中所有的委派都是繼承MulticastDelegate ,而 MulticastDelegate則繼承自Delegate,所以我們可以在一 個事件使用 += 加入委派或使用 -= 刪除委派 button1.Click += new EventHandler(OtherMethod); 事件 委派(內建) 加入方法參考位置到 委派執行清單中
建立與執行委派方式
委派的宣告方式如下,並且可視情況加入參數 步驟1:宣告可以接受一個字串參數的委派(對應的方法也要有一 個字串參數) public delegate void MyDelegate(string str); 步驟2:宣告好委派後,就必須產生委派指向的對應方法,並且 將此方法傳入 Delegate 中,如下: MyDelegate myDelegate = new MyDelegate(ShowMsg); MyDelegate myDelegate = ShowMsg; private void ShowMsg(string pMsg) { MessageBox.Show(pMsg); } 標準寫法 簡化的寫法 委派對應方法的參數與回傳型態需與 委派設定一致
步驟3:呼叫 Invoke() 方法執行委派 方法1:委派.Invoke(參數) 或 委派(參數) myDelegate.Invoke("Hello"); 或 方法2:物件.Invoke(委派,參數) 簡化的寫法 委派 傳入參數 配合執行緒使用
一個簡單的委派範例 宣告委派 建立2個方法 public delegate void MyDelegate(string name); public static void Method1(string value) { MessageBox.Show("Method1 : "+ value); } public static void Method2(string value) MessageBox.Show("Method2 :"+ value);
委派綁定事件與執行 private void Form1_Load(object sender, EventArgs e) { // 標準寫法 MyDelegate UseDel1 = new MyDelegate(Method1); // .NET 2.0 之後可以簡化成 MyDelegate UseDel2 = Method1; //將方法加入委派物件的執行方法清單中 // 在UseDel1身上再綁定1個方法(共綁定2個方法) UseDel1 += new MyDelegate(Method2); //使用 Invoke 方法叫用委派。 UseDel1.Invoke("MyHello!"); UseDel2.Invoke("YourHello!"); //可再簡化成 UseDel1("MyHello!"); UseDel2("YourHello!"); } 單一事件引發多個事件
這不需要委派也可以!
先看個一定需要委派的匿名呼叫 延續上個例子,再加上 .NET 3.0 之後才有 延續上個例子,再加上 這就等於 UseDel1 += (string x) => { MessageBox.Show(x); }; UseDel1("ttt"); Method3不見了! UseDel1 += new MyDelegate(Method3); public static void Method3(string value) { MessageBox.Show("Method3 :"+ value); } 後面我們再談這部分(匿名函數或Lambda)
Delegate委派應用 前面的User Control與Component中已經看過委派的 應用 其他還有當我們需要進行2個視窗互相傳遞資訊時: 例如從A視窗呼叫B視窗事件,或是B視窗的值需要 傳到A視窗時,就可以使用委派的方式處理 委派大部分應用,是將我們自己的function/方法當 成參數,傳到另一個function/方法來執行
跨視窗或跨類別(User Control)呼叫就需要委派 透過委派 讓2個視窗互相傳遞資訊 跨視窗或跨類別(User Control)呼叫就需要委派
讓A視窗與B視窗可互相傳遞資料-B回傳A public delegate void ReturnValueDelegate(string pValue); 接著在 B 視窗宣告一個委派的事件 public event ReturnValueDelegate ReturnValueCallback; 最後在 B 視窗我們要透過一個按鈕觸發委派的事件並將要回傳的 參數傳入 private void button1_Click(object sender, EventArgs e) { ReturnValueCallback(textBox1.Text); }
接著要在A視窗中連結B視窗的事件,先加入一個對應B中 回調的方法,請注意傳入的參數型態與個數必須要與當 初宣告的委派一樣 private void SetReturnValueCallbackFun(string pValue) { textBox1.Text = pValue; }
最後在A視窗開啟B視窗的事件中綁定指定的委派 事件,傳入事件觸發後委派所要執行的方法 private void button1_Click(object sender, EventArgs e) { Form2 frm2 = new Form2(); frm2.ReturnValueCallback += new Form2.ReturnValueDelegate(this.SetReturnValueCallbackFun); frm2.Show(); } 將我們的function/方法當成參數,傳到另一個function/方法來執行
反向,A傳入B呢?
A傳入B 首先在 A 視窗宣告一個委派存放要呼叫的方法的參考 public delegate void SendValueDelegate(string pValue); 接著在 A 視窗宣告一個委派的事件 public event SendValueDelegate SendValueCallback; 跳到 B 視窗,在 B 視窗中加入一個接收傳入參數的方法,讓 SendValueDelegate 委派指向此方法 public void ReceiveValueCallbackFun(string pValue) { textBox1.Text = pValue; }
最後在A視窗按鈕點擊開啟B視窗的事件中,綁定 此回調事件指向 B 視窗的 ReceiveValueCallbackFun 方法,在調用此回調事件傳入參數 private void button2_Click(object sender, EventArgs e) { Form2 frm1 = new Form2(); this.SendValueCallback += new SendValueDelegate(frm2.ReceiveValueCallbackFun); this.SendValueCallback(textBox1.Text); frm2.ShowDialog(); }
需要處理視窗開啟與重複開啟的問題!
委派大部分應用,是將我們自己的function/方法當成參數,傳到另 一個function/方法來執行 另一個範例 委派大部分應用,是將我們自己的function/方法當成參數,傳到另 一個function/方法來執行
TextBox Multiline=true
作法 作法1:在每個按鈕上撰寫各自的程式,完成後回 傳結果到TextBox上,幾個按鈕就有幾段程式 作法2:只使用一個方法,透過傳入參數的方式決 定要執行的段落,好處是只要維護一個很大段的方 法 作法3:透過委派來執行
透過委派執行分享資訊 ShareToFB方法 ShareToLine方法 ShareToTwitter方法 呼叫方式相同 透過委派分配 觸發動作 呼叫AssignShare方法 傳遞資訊與對象方法 AssignShare ShareToLine方法 ShareMSG委派 ShareToTwitter方法 呼叫方式相同 透過委派分配 前後端可以分別開發,要加入與移除一個方法是簡單與方便的
先建立委派 // 建立委派 public delegate string ShareMSG(string msg); 需要回傳 需要傳送
建立呼叫的唯一方法 // 執行分享分派動作 // msg是要分享的資訊 private void AssignShare(string msg, ShareMSG doAct) { textBox1.Text = string.Empty; // 最後的結果 string finalResult; //不需要知道這個doAct是什麼(可以給別人寫) //反正doAct跑完會回傳一個值給我們 finalResult = doAct(msg); textBox1.Text += finalResult; } 委派的對象方法 一個中性的轉介方法
分享到FB的方法 // 分享到FB private string ShareToFB(string msg) { // 自訂分享動作 var res = @" 需要先建立應用程式 取得FB token 串接API 傳送{0}...... "; string finalresult = string.Format(res, msg); // 回傳最後結果 return finalresult; }
分享到Line的方法 // 分享到Line private string ShareToLine(string msg) { // 自訂分享動作 var res = @" 取得API Key 串接API 傳送{0}...... "; string finalresult = string.Format(res, msg); // 回傳最後結果 return finalresult; }
分享到Twitter的方法 // 分享到 Twitter private string ShareToTwitter(string msg) { // 自訂分享動作 var res = @" 沒做過! 傳送{0}...... "; string finalresult = string.Format(res, msg); // 回傳最後結果 return finalresult; }
按鈕觸發動作 private void button1_Click(object sender, EventArgs e) { AssignShare("到FB的訊息", ShareToFB); } private void button2_Click(object sender, EventArgs e) AssignShare("到LINE的訊息", ShareToLine); private void button3_Click(object sender, EventArgs e) AssignShare("到TWITTER的訊息", ShareToTwitter);
result
未來 鬆散耦合 如果又多了其他分享方法,主要的分配方法AssignShare 完全不用動 只要先寫好要分享的對象方法(例如前面的ShareToFB、 ShareToLine、ShareToTwitter),然後呼叫分配方法 AssignShare,傳入訊息與對象方法就可以 要去除、新增與維護上都較簡便 適用在有多個方法需要呼叫,每個方法都有相同的參數 與回傳型態,只是執行內容不同
可以將後端的方法類別化 大家就可以分別開發
Thread執行緒 .NET Framework 2.0後
執行緒 在現代的作業系統中,如果我們將一個程式重複執行兩次,那麼 這兩個程式將是絲毫不相關的。任何一個程式都不需要知道另一 個程式是否存在,通常也不會與另一個程式進行溝通 但是,如果我們希望兩個程式能夠互相溝通,但是卻又要同時執 行,此時就可以利用 Thread 的機制 又如果有一個很耗資源需要執行很久的程序,為了避免影響正常 功能的使用,我們可以把它切到其他的執行緒去處理 執行緒解決了多個應用程式共用同一顆 CPU 所產生的問題,但也 必須付出一點代價,包括空間(記憶體損耗)與時間(執行效能)
前台與後台執行緒 預設狀態下,建立一個Thread物件是屬於前台Thread,表 示只要這個Thread物件還在執行,主程序就會等待其執行 完畢 如果建立的Thread被設定為後台Thread,那主程序並不會 等待其執行完畢,也就是後台Thread可能根本沒機會被執 行完畢
委派與執行緒執行機制 前面說明的事件與委派並不牽涉到執行緒
執行緒(Thread)與委派(delegate) C#執行緒的使用方式與委派(delegate)分不開 需先將要執行的工作設定委派 再透過執行緒執行委派的方法
Thread與委派執行方式(沒有參數) 使用執行緒時要先匯入System.Threading using System.Threading; 建立需要在執行緒上的工作(需委派的方法) void MethodRunInThread() { string result= string.Format("Hello,Thread{0}", Thread.CurrentThread.ManagedThreadId.ToString()); MessageBox.Show(result); } //Thread.CurrentThread.ManagedThreadId 是系統自動取得的目前執行緒 名稱
建立ThreadStart委派(這是內建的委派),它是用來委派要在執行緒 上執行的方法 ThreadStart ts = new ThreadStart(MethodRunInThread); //委派方法 建立Thread並將所要執行的委派當作參數 Thread tt = new Thread(ts); 啟動執行緒 tt.Start(); 這樣啟動的就是前台執行緒
CODE
前台與後台Thread private void Form1_Load(object sender, EventArgs e) { Console.WriteLine("Main Thread start"); // 建立前台Thread Thread t1 = new Thread(DoRun1); t1.Start(); // 建立後台Thread Thread t2 = new Thread(DoRun2) { IsBackground = true }; t2.Start(); Application.Exit(); } private void DoRun1() Thread.Sleep(500); Console.WriteLine("這是前台Thread"); private void DoRun2() Thread.Sleep(1500); Console.WriteLine("這是後台Thread"); 前台與後台Thread
result 並沒有看到後台Thread被執行完成
ThreadPool 設定ThreadPool使用的最大數量 加入任務到ThreadPool ThreadPool.SetMaxThreads(int workerThreads,int completionPortThreads) 加入任務到ThreadPool ThreadPool.QueueUserWorkItem(new WaitCallback(方法名稱),參數);
private void Form1_Load(object sender, EventArgs e) { Console.WriteLine("Main Thread start"); // 建立前台Thread for (int i=0; i<10;i++) Thread t1 = new Thread(DoRun1); t1.Start(); } ThreadPool.SetMaxThreads(5, 5); for (int j = 0; j < 10; j++) ThreadPool.QueueUserWorkItem(new WaitCallback(DoRun2),j); private void DoRun1() Console.WriteLine("這是前台Thread" + Thread.CurrentThread.ManagedThreadId); private void DoRun2(object x) Console.WriteLine("Threadpool" + x.ToString() + "/" + Thread.CurrentThread.ManagedThreadId);
result 使用標準Thread 使用ThreadPool
更簡單的寫法 使用Parallel.For 自動會使用ThreadPool for (int j = 0; j < 10; j++) { ThreadPool.QueueUserWorkItem(new WaitCallback(DoRun2),j); } Parallel.For(0,10,j=> { Console.WriteLine("parallel" + j.ToString() + "/" + Thread.CurrentThread.ManagedThreadId); });
MSDN for Parallel https://msdn.microsoft.com/zh-tw/library/system.threading.tasks.parallel(v=vs.110).aspx
Task .NET Framework 4.0後推出的Thread機制 Task預設啟動的是後台Thread,不過可以透過Wait 方法讓主程序等待Task執行完畢 Task使用的是Thread Pool
Task vs. Wait private void Form1_Load(object sender, EventArgs e) { Console.WriteLine("啟動主程序"); // 使用Task建立後台Thread Task.Run(() => { Thread.Sleep(1000); Console.WriteLine("Task1完成"); }); // 透過Wait方法讓主程序等待其執行完成 Task task = Task.Run(() => { Thread.Sleep(1500); Console.WriteLine("Task2完成"); }); task.Wait(); Application.Exit(); }
Thread Pool 改用Task private void Form1_Load(object sender, EventArgs e) { Console.WriteLine("啟動主程序"); for (int i = 0; i < 10; i++) new Thread(DoRun1).Start(); } Task.Run(() => { DoRun2(); }); private void DoRun1() Thread.Sleep(500); Console.WriteLine("這是前台Thread" + Thread.CurrentThread.ManagedThreadId); private void DoRun2() Thread.Sleep(1500); Console.WriteLine("這是後台Thread" + Thread.CurrentThread.ManagedThreadId); Thread Pool 改用Task
Task<TResult> 執行Task後需要有返回值時使用 呼叫task.Result的效果與Wait方法相同,也就是主程序會等待Task 完成 private void Form1_Load(object sender, EventArgs e) { Console.WriteLine("啟動主程序"); Task<string> task = Task<string>.Run(() => { Thread.Sleep(1000); return Thread.CurrentThread.ManagedThreadId.ToString(); }); Console.WriteLine(task.Result); }
Task、async與await的關係
如果要傳送參數到執行緒內部 需要使用到 ParameterizedThreadStart ParameterizedThreadStart與ThreadStart相同,只是多了參 數傳遞機制 與ThreadStart相同也是一個C#已經定義好的委派 Public delegate void ParameterizedThreadStart(Object obj)
修改要在執行緒上執行的方法 修改要在執行緒上執行的方法(需委派的方法如果參數,類型必須 為Object,與ThreadPool相同) void MethodRunInThread(object str) { string result= string.Format("Hello{0},Thread{1}", str, Thread.CurrentThread.ManagedThreadId.ToString()); MessageBox.Show(result); }
建立委派方法 建立ParameterizedThreadStart委派並委派方法 ParameterizedThreadStart ts = new ParameterizedThreadStart(MethodRunInThread); 建立Thread並將所要執行的委派當作參數 Thread tt = new Thread(ts); 啟動執行緒並傳入參數 tt.Start("CTUIM");
code
result
Thread vs. Update UI
跨執行緒控制無效… 如果在Visual C#中使用多個執行緒,要從某個執行 緒更新介面時,會出現跨執行緒控制無效…的錯誤 原因就是UI的更新是在主執行緒上,其他執行緒需 要透過委派更新
如果將Messagebox改成Label
跨執行緒控制無效…
Visual C#的解決方式有 方法1:直接改變 Form表單的屬性,此作法比較簡單,但 較不安全,不過若程式並不複雜,此方法較簡單,直接 將下面這行撰寫在Form_Load事件中即可 Form.CheckForIllegalCrossThreadCalls = false; 方法2:採取委派的方式,此方法較正統,但撰寫上較複 雜
透過委派更新介面 // 建立委派 private delegate void UpdateUICallBack(string value, Control ctl); // 建立委派要執行的方法 private void UpdateUI(string value, Control ctl) { if (this.InvokeRequired) { UpdateUICallBack uu = new UpdateUICallBack(UpdateUI); this.Invoke(uu , value, ctl); } else { ctl.Text = value; 在需要更新介面時呼叫 UpdateUI 即可 判斷呼叫更新者與建立UI者是否同一個執行緒
改成透過委派更新
Thread vs. 耗時工作
建立一個耗時的類別HardWork class HardWork { public static TextBox _textbox; // 透過委派呼叫別的執行緒 delegate void PrintHandler(TextBox tb, string text); public static void Run() //這裡是一個無窮迴圈 int x = 0; while (true) if (x % 1000 == 0) // 呼叫別的執行緒上的控制項進行顯示 ShowMsg(_textbox, x.ToString()); } x++;
public static void ShowMsg(TextBox tb, string text) { // 判斷TextBox是否在同一個執行緒上 if (tb.InvokeRequired) //當InvokeRequired為true時,表示在不同的執行緒上,所以進行委派的動作 PrintHandler ph = new PrintHandler(ShowMsg); tb.Invoke(ph, tb, text); } else //表示在同一個執行緒上了,所以可以正常的呼叫到這個TextBox物件 tb.Text = text + Environment.NewLine;
透過Thread執行/停止耗時的類別
//新的執行緒 Thread othread; private void button1_Click(object sender, EventArgs e) { // 將這個Form上的TextBox指定給HardWork._textbox這個變數 // 因為_textbox設定為static HardWork._textbox = textBox1; // 告訴這個執行緒,該去執行這個方法 othread = new Thread(HardWork.Run); // 開始 othread.Start(); } private void button2_Click(object sender, EventArgs e) { //停止執行緒的運作!!! othread.Abort(); }
Summary 需要使用委派的幾個時機 使用匿名函式時 單一事件引發多重事件 跨表單或類別(User Control)呼叫方法時(將我們 自己的function/方法當成參數,傳到另一個 function/方法來執行) 簡化方法呼叫時 配合執行緒使用