多線程調用,任務線程拋出異常如何在另一個線程(調用線程)中捕獲併進行處理的問題。 ...
記一次Task拋異常,調用線程處理而引發的一些隨想
多線程調用,任務線程拋出異常如何在另一個線程(調用線程)中捕獲併進行處理的問題。
1.任務線程在任務線程執行語句上拋出異常。
例如:
1 private void button2_Click(object sender, EventArgs e) 2 { 3 try 4 { 5 var task = Task.Factory.StartNew<bool>(() => 6 { 7 //Do Some Things 8 throw new Exception("Task Throw Exception!"); 9 //return true; 10 }); 11 12 //var result = task.Wait(20000); 13 var result = task.Result; 14 } 15 catch (Exception ex) 16 { 17 18 } 19 20 }
調試結果:在Task.Rrsult或者Wait時可以拋出任務異常,併在調用線程中通過try-catch捕獲處理。
2.任務線程在非同步委托執行語句上拋出異常。
1 private void button3_Click(object sender, EventArgs e) 2 { 3 var fun = new Func<int>(() => 4 { 5 //do sonmething 6 throw new Exception("Task Throw Exception!"); 7 return 1; 8 }); 9 try 10 { 11 var task = Task.Factory.StartNew<bool>(() => 12 { 13 try 14 { 15 var res = fun.BeginInvoke(null, null); 16 //do some thing 17 var ob = fun.EndInvoke(res); 18 } 19 catch (Exception ex) 20 { 21 22 throw ex; 23 } 24 return true; 25 }); 26 var result = task.Wait(20000); 27 //var result1 = task.Result; 28 } 29 catch (Exception ex) 30 { 31 32 } 33 }
調試可知:非同步委托在調用EndInvoke(res)獲取結果時可以捕獲委托內部異常並拋出由外部Task抓取。
2.任務線程在視窗句柄(創建控制項)線程上拋異常現象。
control.invoke(參數delegate)方法:在擁有此控制項的基礎視窗句柄的線程上執行指定的委托。
control.begininvoke(參數delegate)方法:在創建控制項的基礎句柄所線上程上非同步執行指定委托。
即invoke表是同步、begininvoke表示非同步。但是如何來進行同步和非同步呢?
2.1Invoke方法執行規則
Invoke的原理是藉助消息迴圈通知主線程,並且在主線程執行委托。直接代碼查看:
1 private void button1_Click(object sender, EventArgs e) 2 { 3 //Invoke的原理是藉助消息迴圈通知主線程,並且在主線程執行委托。 4 try 5 { 6 var thIdMain = Thread.CurrentThread.ManagedThreadId; 7 Console.WriteLine($"Load start: Main Thread ID:{thIdMain}"); 8 var task = Task.Factory.StartNew<bool>(() => 9 { 10 var taskId = Thread.CurrentThread.ManagedThreadId; 11 Console.WriteLine($"Task start: Task Thread ID:{taskId}"); 12 var res = this.Invoke(new Func<int>(() => 13 { 14 var InvokeId = Thread.CurrentThread.ManagedThreadId; 15 Console.WriteLine($"Invoke in: Begion Invoke Thread ID:{InvokeId}"); 16 //do sonmething 17 return 1; 18 })); 19 taskId = Thread.CurrentThread.ManagedThreadId; 20 Console.WriteLine($"Invoke out ,Thread ID:{taskId}"); 21 return true; 22 23 }); 24 25 thIdMain = Thread.CurrentThread.ManagedThreadId; 26 Console.WriteLine($"Wait: Main Thread ID:{thIdMain}"); 27 var CanLoad = task.Wait(2000);//.Result; 28 thIdMain = Thread.CurrentThread.ManagedThreadId; 29 Console.WriteLine($"End: Main Thread ID:{thIdMain}"); 30 } 31 catch (Exception) { } 32 }
執行輸出:
Load start: Main Thread ID:1
Wait: Main Thread ID:1
Task start: Task Thread ID:3
End: Main Thread ID:1
Invoke in: Begion Invoke Thread ID:1
Invoke out ,Thread ID:3
查看輸出順序說明:invoke在主線程中執行,但是,invoke後面的代碼必須在Invoke委托方法執行完成後,才能繼續執行;而invoke在主線程中執行,所以其執行時機無法確定,得等消息迴圈(主線程)中其它消息執行後才能進行。
2.2BeginInvoke方法執行規則
BeginInvoke也在主線程執行相應委托。直接代碼查看:
1 private void button1_Click(object sender, EventArgs e) 2 { 3 //BeginInvoke 4 try 5 { 6 var thIdMain = Thread.CurrentThread.ManagedThreadId; 7 Console.WriteLine($"Load start: Main Thread ID:{thIdMain}"); 8 var task = Task.Factory.StartNew<bool>(() => 9 { 10 var taskId = Thread.CurrentThread.ManagedThreadId; 11 Console.WriteLine($"Task start: Task Thread ID:{taskId}"); 12 var res = this.BeginInvoke(new Func<int>(() => 13 { 14 var BegionInvokeId = Thread.CurrentThread.ManagedThreadId; 15 Console.WriteLine($"BeginInvoke in: Begion Invoke Thread ID:{BegionInvokeId}"); 16 //do sonmething 17 return 1; 18 })); 19 taskId = Thread.CurrentThread.ManagedThreadId; 20 Console.WriteLine($"BeginInvoke is Completed: {res.IsCompleted}, Thread ID:{taskId}"); 21 var ob = this.EndInvoke(res); 22 taskId = Thread.CurrentThread.ManagedThreadId; 23 Console.WriteLine($"BeginInvoke out ,Thread ID:{taskId}"); 24 // Console.WriteLine(ob.ToString()); 25 return true; 26 }); 27 long i = 0; 28 while (i < 1000000000)//延時 29 { 30 i++; 31 } 32 thIdMain = Thread.CurrentThread.ManagedThreadId; 33 Console.WriteLine($"Wait: Main Thread ID:{thIdMain}"); 34 //var CanLoad = task.Wait(2000);//.Result; 35 thIdMain = Thread.CurrentThread.ManagedThreadId; 36 Console.WriteLine($"End: Main Thread ID:{thIdMain}"); 37 //Console.WriteLine(CanLoad); 38 } 39 catch (Exception) { } 40 }
執行輸出:
Load start: Main Thread ID:1
Task start: Task Thread ID:3
BeginInvoke is Completed: False, Thread ID:3
Wait: Main Thread ID:1
End: Main Thread ID:1
BeginInvoke in: Begion Invoke Thread ID:1
BeginInvoke out ,Thread ID:3
根據輸出結果可知begininvoke所提交的委托方法也是在主線程中執行,BeginInvoke is Completed: False, Thread ID:3與Wait: Main Thread ID:1兩段比較,會發現begininvoke提交委托方法後,子線程繼續執行,不需要等待委托方法的完成。
總結:invoke和begininvoke都是在主線程中執行。invoke提交的委托方法執行完成後,才能繼續執行;begininvoke提交委托方法後,子線程繼續執行。invoke(同步)和begininvoke(非同步)的含義,是相對於子線程而言的,實際上對於控制項的調用總是由主線程來執行。
2.3 Control.BeginInvoke或者Control.Invoke執行委托時拋出異常
Control.Invoke執行委托時拋出異常:
1 private void button2_Click(object sender, EventArgs e) 2 { 3 try 4 { 5 var task = Task.Factory.StartNew<bool>(() => 6 { 7 try 8 { 9 //Do Some Things 10 var res = this.Invoke(new Func<int>(() => 11 { 12 //do sonmething 13 throw new Exception("Task Throw Exception!"); 14 return 1; 15 })); 16 } 17 catch (Exception ex) 18 { 19 20 throw ex; 21 } 22 return true; 23 }); 24 } 25 catch (Exception ex) 26 { 27 28 } 29 }
執行結果:只有task中的try可以捕捉,繼續拋出,主線程捕捉不到
原因分析:button2_Click方法和task中invoke都是在主線程中執行。但是,invoke必須等主線程中其它消息執行完即button2_Click代碼執行完退出才有機會執行。此時button2_Click方法執行完,所分配的記憶體空間被回收(失效),故即便task繼續拋異常均不能捕獲到。而Invoke在執行完成時,task後續代碼阻斷並等待其執行完,後續執行代碼與其在記憶體上屬於同一 堆棧,故可以捕獲到Invoke拋出的異常。
Control.BeginInvoke執行委托時拋出異常:
1 private void button2_Click(object sender, EventArgs e) 2 { 3 try 4 { 5 var task = Task.Factory.StartNew<bool>(() => 6 { 7 try 8 { 9 //Do Some Things 10 var res = this.BeginInvoke(new Func<int>(() => 11 { 12 //do sonmething 13 throw new Exception("Task Throw Exception!"); 14 return 1; 15 })); 16 17 var ob = this.EndInvoke(res); 18 } 19 catch (Exception ex) 20 { 21 22 throw ex; 23 } 24 return true; 25 }); 26 } 27 catch (Exception ex) 28 { 29 30 } 31 32 }
執行結果:均無法捕捉異常。但是Main函數中可以。
原因分析:button2_Click方法和task中BeginInvoke都是在主線程中執行。但是,BeginInvoke須等主線程中其它消息執行完即button2_Click代碼執行完退出才有機會執行。此時button2_Click方法執行完,所分配的記憶體空間被回收(失效),故即便task繼續跑異常均不能捕獲到。而BeginInvoke在執行完成時,task後續代碼無須阻斷等待其執行完,二者在記憶體上不屬於同一 堆棧, 而非同步調用時,非同步執行期間產生的異常由CRL庫捕獲,你並一般在調用EndInvoke函數獲取執行結果時CRL會拋出引發非同步執行期間產生的異常,但是,CRL對Control.BeginInvoke特殊處理並未拋出(個人猜想,待驗證)。故此時Task無法捕獲到BeginInvoke拋出的異常。
一般BeginInvoke與Invoke主要用於更新控制項相關屬性值,特意拋異常的可能性應該比較小,如果有異常可以在該委托裡面就進行解決了。此處僅作對技術研究的一個記錄。