多线程调用, 任务线程抛出异常如何在另一个线程 (调用线程) 中捕获并进行处理的问题.
1. 任务线程在任务线程执行语句上抛出异常.
例如:
- private void button2_Click(object sender, EventArgs e)
- {
- try
- {
- var task = Task.Factory.StartNew<bool>(() =>
- {
- //Do Some Things
- throw new Exception("Task Throw Exception!");
- //return true;
- });
- //var result = task.Wait(20000);
- var result = task.Result;
- }
- catch (Exception ex)
- {
- }
- }
调试结果: 在 Task.Rrsult 或者 Wait 时可以抛出任务异常, 并在调用线程中通过 try-catch 捕获处理.
2. 任务线程在异步委托执行语句上抛出异常.
- private void button3_Click(object sender, EventArgs e)
- {
- var fun = new Func<int>(() =>
- {
- //do sonmething
- throw new Exception("Task Throw Exception!");
- return 1;
- });
- try
- {
- var task = Task.Factory.StartNew<bool>(() =>
- {
- try
- {
- var res = fun.BeginInvoke(null, null);
- //do some thing
- var ob = fun.EndInvoke(res);
- }
- catch (Exception ex)
- {
- throw ex;
- }
- return true;
- });
- var result = task.Wait(20000);
- //var result1 = task.Result;
- }
- catch (Exception ex)
- {
- }
- }
调试可知: 异步委托在调用 EndInvoke(res)获取结果时可以捕获委托内部异常并抛出由外部 Task 抓取.
2. 任务线程在窗口句柄 (创建控件) 线程上抛异常现象.
control.invoke(参数 delegate)方法: 在拥有此控件的基础窗口句柄的线程上执行指定的委托.
control.begininvoke(参数 delegate)方法: 在创建控件的基础句柄所在线程上异步执行指定委托.
即 invoke 表是同步, begininvoke 表示异步. 但是如何来进行同步和异步呢?
2.1Invoke 方法执行规则
Invoke 的原理是借助消息循环通知主线程, 并且在主线程执行委托. 直接代码查看:
- private void button1_Click(object sender, EventArgs e)
- {
- //Invoke 的原理是借助消息循环通知主线程, 并且在主线程执行委托.
- try
- {
- var thIdMain = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"Load start: Main Thread ID:{thIdMain}");
- var task = Task.Factory.StartNew<bool>(() =>
- {
- var taskId = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"Task start: Task Thread ID:{taskId}");
- var res = this.Invoke(new Func<int>(() =>
- {
- var InvokeId = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"Invoke in: Begion Invoke Thread ID:{InvokeId}");
- //do sonmething
- return 1;
- }));
- taskId = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"Invoke out ,Thread ID:{taskId}");
- return true;
- });
- thIdMain = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"Wait: Main Thread ID:{thIdMain}");
- var CanLoad = task.Wait(2000);//.Result;
- thIdMain = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"End: Main Thread ID:{thIdMain}");
- }
- catch (Exception) { }
- }
执行输出:
- 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 也在主线程执行相应委托. 直接代码查看:
- private void button1_Click(object sender, EventArgs e)
- {
- //BeginInvoke
- try
- {
- var thIdMain = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"Load start: Main Thread ID:{thIdMain}");
- var task = Task.Factory.StartNew<bool>(() =>
- {
- var taskId = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"Task start: Task Thread ID:{taskId}");
- var res = this.BeginInvoke(new Func<int>(() =>
- {
- var BegionInvokeId = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"BeginInvoke in: Begion Invoke Thread ID:{BegionInvokeId}");
- //do sonmething
- return 1;
- }));
- taskId = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"BeginInvoke is Completed: {res.IsCompleted}, Thread ID:{taskId}");
- var ob = this.EndInvoke(res);
- taskId = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"BeginInvoke out ,Thread ID:{taskId}");
- // Console.WriteLine(ob.ToString());
- return true;
- });
- long i = 0;
- while (i <1000000000)// 延时
- {
- i++;
- }
- thIdMain = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"Wait: Main Thread ID:{thIdMain}");
- //var CanLoad = task.Wait(2000);//.Result;
- thIdMain = Thread.CurrentThread.ManagedThreadId;
- Console.WriteLine($"End: Main Thread ID:{thIdMain}");
- //Console.WriteLine(CanLoad);
- }
- catch (Exception) { }
- }
执行输出:
- 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 执行委托时抛出异常:
- private void button2_Click(object sender, EventArgs e)
- {
- try
- {
- var task = Task.Factory.StartNew<bool>(() =>
- {
- try
- {
- //Do Some Things
- var res = this.Invoke(new Func<int>(() =>
- {
- //do sonmething
- throw new Exception("Task Throw Exception!");
- return 1;
- }));
- }
- catch (Exception ex)
- {
- throw ex;
- }
- return true;
- });
- }
- catch (Exception ex)
- {
- }
- }
执行结果: 只有 task 中的 try 可以捕捉, 继续抛出, 主线程捕捉不到
原因分析: button2_Click 方法和 task 中 invoke 都是在主线程中执行. 但是, invoke 必须等主线程中其它消息执行完即 button2_Click 代码执行完退出才有机会执行. 此时 button2_Click 方法执行完, 所分配的内存空间被回收(失效), 故即便 task 继续抛异常均不能捕获到. 而 Invoke 在执行完成时, task 后续代码阻断并等待其执行完, 后续执行代码与其在内存上属于同一 堆栈, 故可以捕获到 Invoke 抛出的异常.
Control.BeginInvoke 执行委托时抛出异常:
- private void button2_Click(object sender, EventArgs e)
- {
- try
- {
- var task = Task.Factory.StartNew<bool>(() =>
- {
- try
- {
- //Do Some Things
- var res = this.BeginInvoke(new Func<int>(() =>
- {
- //do sonmething
- throw new Exception("Task Throw Exception!");
- return 1;
- }));
- var ob = this.EndInvoke(res);
- }
- catch (Exception ex)
- {
- throw ex;
- }
- return true;
- });
- }
- catch (Exception ex)
- {
- }
- }
执行结果: 均无法捕捉异常. 但是 Main 函数中可以.
原因分析: button2_Click 方法和 task 中 BeginInvoke 都是在主线程中执行. 但是, BeginInvoke 须等主线程中其它消息执行完即 button2_Click 代码执行完退出才有机会执行. 此时 button2_Click 方法执行完, 所分配的内存空间被回收(失效), 故即便 task 继续跑异常均不能捕获到. 而 BeginInvoke 在执行完成时, task 后续代码无须阻断等待其执行完, 二者在内存上不属于同一 堆栈, 而异步调用时, 异步执行期间产生的异常由 CRL 库捕获, 你并一般在调用 EndInvoke 函数获取执行结果时 CRL 会抛出引发异步执行期间产生的异常, 但是, CRL 对 Control.BeginInvoke 特殊处理并未抛出(个人猜想, 待验证). 故此时 Task 无法捕获到 BeginInvoke 抛出的异常.
一般 BeginInvoke 与 Invoke 主要用于更新控件相关属性值, 特意抛异常的可能性应该比较小, 如果有异常可以在该委托里面就进行解决了. 此处仅作对技术研究的一个记录.
来源: https://www.cnblogs.com/VueDi/p/11527443.html