C#如何优雅地取消一个流程(非Thread.Abort方法)

一. Thread.Abort() 的缺点

我们使用 Thread.Abort() 来中止一个包裹着某个流程的线程,虽然 C# 并不会像 Thread.Suspend() 提示过时。但是在使用 Thread.Abort() 的时候,确实存在很多的问题:

1. 该方式中止线程是通过在线程执行的时候抛出 ThreadAbortException 异常来实现的。这边抛出的 ThreadAbortException 异常,不一定可以被局部的程序异常处理程序准确地捕获,而会被抛出在全局,需要通过 AppDomain.CurrentDomain.UnhandledException 来进行捕获处理;

注:关于 AppDomain.CurrentDomain.UnhandledException 的使用,请看下面的文章:

《关于C# 全局异常捕获》

2. 不能确定线程中止的时间和在中止之前所执行到的位置。总之,它取消线程是一个队列的处理方式,所以处理不是被立即响应的,而是要等一会,这就导致了程序的中止时间和在中止之前执行到了哪里都是未知的;

二. 代替方法

  • 那么我们有没有什么方法来代替这种 Thread.Abort() 的方式呢?

答:有。分析:中止一个流程,我们一定要向这个流程发送一个中止的信号,当这个流程获得这个信号的时候,立刻中断之后的操作并返回。因此,我们可以分析出来,这个流程在工作的时候,起码要有两个角色

  1. 一个是用来处理流程,也就是业务;
  2. 一个是用来处监视取消信号,并终止流程;

综上,我们可以考虑使用委托的异步调用来实现,如果要取消就使用轮序信号来实现。

三. 代码

  • 实现委托异步调用的类 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();

五. 示例代码下载

下载地址

posted @ 2020-02-03 17:52  霁雪湖上三映月  阅读(2820)  评论(0编辑  收藏  举报