C#如何优雅地取消一个流程(非Thread.Abort方法)
一. Thread.Abort() 的缺点
我们使用 Thread.Abort() 来中止一个包裹着某个流程的线程,虽然 C# 并不会像 Thread.Suspend() 提示过时。但是在使用 Thread.Abort() 的时候,确实存在很多的问题:
1. 该方式中止线程是通过在线程执行的时候抛出 ThreadAbortException 异常来实现的。这边抛出的 ThreadAbortException 异常,不一定可以被局部的程序异常处理程序准确地捕获,而会被抛出在全局,需要通过 AppDomain.CurrentDomain.UnhandledException 来进行捕获处理;
注:关于 AppDomain.CurrentDomain.UnhandledException 的使用,请看下面的文章:
2. 不能确定线程中止的时间和在中止之前所执行到的位置。总之,它取消线程是一个队列的处理方式,所以处理不是被立即响应的,而是要等一会,这就导致了程序的中止时间和在中止之前执行到了哪里都是未知的;
二. 代替方法
- 那么我们有没有什么方法来代替这种 Thread.Abort() 的方式呢?
答:有。分析:中止一个流程,我们一定要向这个流程发送一个中止的信号,当这个流程获得这个信号的时候,立刻中断之后的操作并返回。因此,我们可以分析出来,这个流程在工作的时候,起码要有两个角色:
- 一个是用来处理流程,也就是业务;
- 一个是用来处监视取消信号,并终止流程;
综上,我们可以考虑使用委托的异步调用来实现,如果要取消就使用轮序信号来实现。
三. 代码
- 实现委托异步调用的类 LongTime.cs 代码:
public class LongTime { public bool isComplete = false; public bool isCancel = false; public bool isSuccess = false; private object asynObject = new object(); private bool LongTimeMethod() { lock (asynObject) { if(this.isCancel) { //如果取消了要做什么操作 } Thread.Sleep(10 * 1000); return false; } } public void Test() { Console.WriteLine("开始..."); //异步执行体 AsyncCallback callback = (r) => { this.isComplete = r.IsCompleted; }; IAsyncResult result = ((Action)(() => { this.isSuccess = LongTimeMethod(); })) .BeginInvoke(callback, null); //执行表征体 while (!isCancel && !result.IsCompleted) { Thread.Sleep(50); } Console.WriteLine(this.isCancel ? "取消" : "完成"); } /* * ❤重要❤ * 会处在一个异步的线程中,如果这里面的内容与LongTimeMethod方法中的内容有冲突 * 那么会导致线程先后的次序的问题 * 所以这边要加上lock */ public void Cancel() { lock (asynObject) { this.isCancel = true; //如果执行完成了业务逻辑之后的补救操作 } } }
- 上端调用测试代码:
LongTime longTime = new LongTime(); //随机取消 Thread thread = new Thread(() => { Thread.Sleep(2000); if ((new Random()).Next() % 2 == 0) { longTime.Cancel(); } }); thread.Start(); //开始测试 longTime.Test(); Console.ReadKey();
四. 要点分析
- 我们需要注意什么呢?
答:要注意的是在发送消息之后,更新、判断取消状态的线程异步问题。所以代码要上锁,如下:
- 业务逻辑:
private bool LongTimeMethod() { lock (asynObject) { if(this.isCancel) { //如果取消了要做什么操作 } Thread.Sleep(10 * 1000); return false; } }
- 取消操作:
public void Cancel() { lock (asynObject) { this.isCancel = true; //如果执行完成了业务逻辑之后的补救操作 } }
由于我们无法知道,是 Cancel() 方法先修改 isCancel 标志位的值,还是 LongTimeMethod() 业务方法先判断 isCancel 为 false 并执行业务逻辑代码。所以程序在为后者的情况下,要在后执行的 Cancel() 方法中添加用于直接完成了业务逻辑的补救操作。
- 示例程序,这是一个打开一个窗体程序,并取消前一个窗体之后再次打开一个窗体程序的代码示例
- 窗体程序就是打开一个窗体,名字叫 " DemoForm.exe " 的程序
- 实现异步委托调用的类,也就是业务逻辑类 LongTimeEx.cs,代码如下:
public class LongTimeEx { public static bool isComplete = true; public static bool isCancel = true; public static bool isSuccess = false; private static object asynObject = new object(); private bool LongTimeMethod() { lock (asynObject) { if (isCancel) { return false; } using (Process process = new Process()) { process.StartInfo.FileName = "DemoForm.exe"; process.Start(); } return true; } } public void Test() { Console.WriteLine("开始..."); //判断一下上一个是不是已经结束 while (!LongTimeEx.isComplete || !LongTimeEx.isCancel) { Thread.Sleep(50); } //由于修改为了静态的变量 //导致这边每次都要重新刷新 isComplete = false; isCancel = false; //异步执行体 AsyncCallback callback = (r) => { isComplete = r.IsCompleted; }; IAsyncResult result = ((Action)(() => { isSuccess = LongTimeMethod(); })) .BeginInvoke(callback, null); //执行表征体 while (!isCancel && !result.IsCompleted) { Thread.Sleep(50); } } public void Cancel() { lock(asynObject) { isCancel = true; //取消一下启动的进程 Process[] process = Process.GetProcessesByName("DemoForm"); if(process.Count()>0) { foreach(Process p in process) { p.Kill(); } } } } }
- 上端的调用代码:
LongTimeEx longTimeEx = new LongTimeEx(); longTimeEx.Test(); longTimeEx.Cancel(); longTimeEx.Test(); Console.ReadKey();