zoukankan      html  css  js  c++  java
  • 通过IEnumerable和IDisposable实现可暂停和取消的任务队列

         一般来说,软件中总会有一些长时间的操作,这类操作包括下载文件,转储数据库,或者处理复杂的运算。

      一种处理做法是,在主界面上提示正在操作中,有进度条,其他部分不可用。这里带来很大的问题, 使用者不知道到底执行到什么程度,无法暂停或者取消任务。而即使花了很大的力气实现了暂停和取消,也很难形成通用的模块。

      另一种是类似下载工具那样,有多个在任务队列中的任务,提示用户当前执行了多少,可以选择暂停或者取消任务。如下图:

      显然后者的用户体验更好。那么,如何实现它呢?

      应当考虑,这个任务管理器应当尽可能通用,作为基础类库为上层功能服务,它应该尽量友好,方便上层调用。

      也许你已经猜到了,关键就是枚举器IEnumerable,更多可参考你可能不知道的陷阱, IEnumerable接口

          由于时间仓促,文章写得比较粗略,感兴趣研究的可以在文章末尾下载Demo查看。

    1.可暂停的任务

      首先,我们考虑如何实现暂停。主线程是不能暂停的,否则就无法响应用户操作,因此一定要有主线程之外的工作线程。为了方便,直接创建线程或使用线程池都比较麻烦,我们使用Task来创建新任务。

      暂停一般有两种做法,一种是信号量,一种是一个暂停标记,不断循环检查标记,否则就休眠一定时间。

      显然信号量更方便,消耗资源更少,而且无延迟。  可以使用AutoResetEvent。我们先定义一个任务的基类:    

    复制代码
    public abstract class TaskBase : PropertyChangeNotifier
    {  //暂时省略了其他无关的代码
          public bool IsPause
            {
                get { return _isPause; }
                set
                {
                    if (_isPause != value)
                    {
                        _isPause = value;
                        if (value)
                        {
                            autoReset.Reset();
                        }
                        else
                        {
                            autoReset.Set();
                        }
    
                        OnPropertyChanged("IsPause");
                    }
                }
            }   
    
         public bool CheckWait()
            {
                if (IsPause)
                {
                    autoReset.WaitOne();
                    return true;
                }
                return false;
            }
    }
    复制代码

          在调用时,可以使用类似以下的语句:

     foreach (var task in tasks)
                {
                    CheckWait();  //如果IsPause被设置True,此处自动阻塞
                    action(task);  //执行对task的操作
                }

    2.取消

      可以让一个任务方便的启动,但却很难将其取消。强行终止工作线程,不仅可能不会立刻终止,同时还会引发异常,甚至造成不可预测的结果。所以我们采用尽可能优雅的主动检测。

      如何取消呢?可以使用CancellationTokenSource。 在每次枚举过程中,检查取消标记,如果已经取消,则break当前枚举。类似暂停的方法。

          具体代码与暂停类似,可参考文章最后的Demo.

    3.进度条

      实现进度是比较容易也是困难的事情,要知道整个枚举的数量,通过外部数据来提示它。传入一个当前的位置,求出与整个位置的比值,即可得到进度。

    4.多个任务的任务队列

      我们期望能够形成任务队列,这些任务可以调整执行顺序,还能够顺次或同时执行,根据以上的知识,就可以构造下面的类出来:

    复制代码
    /// <summary>
        /// 任务调度器
        /// </summary>
        public class BatchTaskScheduler
        {
            public BatchTaskScheduler()
            {
                CurrentProcessTasks=new ObservableCollection<TaskBase>();
            }
            /// <summary>
            /// 当前所有执行的任务
            /// </summary>
            public IList<TaskBase> CurrentProcessTasks { get; set; }
            /// <summary>
            /// 添加一个临时任务
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="taskName">任务名称</param>
            /// <param name="enumable">任务枚举器</param>
            /// <param name="action">对枚举的每一个元素执行的操作</param>
            /// <param name="contineAction">枚举完成后执行的操作</param>
            /// <param name="count">可选,枚举总数量,用于指示进度条</param>
            /// <param name="autoStart">是否自动运行</param>
            public void AddTempTask<T>(string taskName, IEnumerable<T> enumable, Action<T> action, Action<int> contineAction = null, int count = -1, bool autoStart = true)
            {
                var tempTask = new TemporaryTask();
                tempTask.Scheduler = this;
                tempTask.Name = taskName;
                tempTask.TaskAction = () =>
                {
    
                    if (enumable is ICollection<T>)
                    {
                        count = (enumable as ICollection<T>).Count;  //此处可能能够获取整个枚举的大小
                    }
                    if (count == 0)
                        count = -1;
                    var finish = false;
                    foreach (var r in enumable)
                    {
                        if (action != null)
                            action(r);
                        tempTask.CheckWait();  
                        if (r is int)
                        {
                            tempTask.CurrentIndex = Convert.ToInt32(r);
                        }
                        else
                        {
                            tempTask.CurrentIndex++;
                        }
    
                        if (count != -1)
                        {
                            tempTask.Percent = tempTask.CurrentIndex * 100 / count;  //计算进度条位置
                        }
                        if (tempTask.CheckCancel())
                        {
                            finish = true;
                            break;
                        }
    
                    }
                    if (finish == true)
                        tempTask.Percent = 100;
                    if (contineAction != null)
                    {
                        ControlExtended.UIInvoke(() => contineAction(tempTask.CurrentIndex));
                    }
    
                };
                this.CurrentProcessTasks.Add(tempTask);
                if (autoStart == true)
                {
    
                    tempTask.Start();
                }
    
    
            }
        }
    复制代码

      使用起来也很方便:

    复制代码
    public IEnumerable<int> TestTask(int count)  //表达一个耗时的函数
            {
                for (int j = 0; j < count; j++)
                {
                    Thread.Sleep(300);
                    yield return j;
                }
            }
      
      private void Button_Click_1(object sender, RoutedEventArgs e)
            {
                int total = 15;
        Scheduler.AddTempTask("任务1:延时测试", TestTask(total));  
         Scheduler.AddTempTask("任务1:延时测试", TestTask(total), null, result => MessageBox.Show(string.Format("延时测试任务已经完成,迭代位置{0}", result)), total); 
    }
    复制代码

       当然,你可以将TestTask函数换成自己的匿名函数。

    5.改造已有的耗时代码(不安全)

      这些耗时代码可能是在类库中已经存在的大量代码,那么,如何能够尽可能方便地修改它们,以适合以上的模式呢?还是枚举器,yield return.

      以写入文件为例,说明如何改造:

    复制代码
     public IEnumerable<int> WriteFileUnsafe(string filename, int count)
            {
                var fs = new FileStream(filename, FileMode.OpenOrCreate);
                var sw = new StreamWriter(new BufferedStream(fs), Encoding.Default);
                int j = 0;for (j = 0; j < count; j++)
                {
                    Thread.Sleep(100);  //模拟耗时任务
                    sw.WriteLine("这个数据是" + j);
                    yield return j;
                }
    sw.Close(); fs.Close();
    yield return j; }
    复制代码

      值得注意的是,这段代码并不会主动执行,由于引入了yield,它的执行需要外部去“推”。因此一个很有可能发生的问题是,如果不去检查返回值,那么这段代码就不会执行!这个确实是违反直觉的。外界用多少就执行多少。

         如果想对其全部执行,可以使用var r= WriteFileUnsafe(filename,100).LastOrDefault(); 这个方法会将枚举推到最后一步。但是,r不使用的话,会不会被编译器优化掉呢?

              细心的读者可能会发现,上面的代码 是不安全的,因为引入了yield,所以try-catch变得鸡肋。同时,一旦用户取消了这个操作,其实资源是没有被回收的!这段代码会在某一次yield之后直接返回,这会造成严重的安全问题! 

         可能有人会想到,通过外界判断是否执行完毕,传入委托告诉调度器如何回收资源。可是,这破坏了代码的一致性。如何回收资源应当是使用资源本身的函数所考虑的,而不应该交给其他类。

    6. 使用IDisposable模式解决安全问题

          为了保证在随时取消任务之后,回收资源的代码被执行,所以必须考虑特别的方法。

          Try-catch代码块是不用想了,因为枚举yield中是不支持的,不能通过抛异常来解决。

          那么就引入using吧,使用IDisposable模式! 定义一个辅助类:

    复制代码
       public class DisposeHelper : IDisposable
        {
            private Action action;
            public DisposeHelper(Action action2)
            {
                action = action2;
    
            }
            public void Dispose()
            {
                action();
            }
        }
    复制代码

      这个类非常简单,只有一个委托。在Dispose的时候调用该委托执行操作。使用起来更是碉堡了:

        

    复制代码
     public IEnumerable<int> WriteFileTask(string filename, int count)
            {
                var fs = new FileStream(filename, FileMode.OpenOrCreate);
    
                var sw = new StreamWriter(new BufferedStream(fs), Encoding.Default);
    
                int j = 0;
                using (var dis = new DisposeHelper(() =>
                {
                    sw.Close();
                    fs.Close();  //不论是抛出异常,还是取消任务,还是正常完成,这段代码一定会被执行
                }))
                {
                    for (j = 0; j < count; j++)
                    {
                        Thread.Sleep(100);
                        sw.WriteLine("这个数据是" + j);
                        yield return j;
                    }
                }
            }
    复制代码

           我们狠狠的舔了一下using这个语法糖。

           基本上有了这些之后,功能就比较全面了。下面上测试样例:

    复制代码
     private void Button_Click_2(object sender, RoutedEventArgs e)
            {
                string fileName = "Test.txt";
                Scheduler.AddTempTask("任务2:写入文件", WriteFileTask(fileName, 100), null,
                    d =>
                    {
                        Process.Start(fileName);
                    }, 100);
            }
    复制代码

       7.性能问题

      使用这种模式之后,我发现,它在做一些操作的时候,会比不使用这种模式来的更慢一些。其原因可能有这么几点:

      (1)修改了CurrentIndex值,而该值通过属性通知方法,不断的通知UI,可能会造成性能损失

      (2)yield模式降低了代码的命中率,使得CPU的跳转大大增加。

      所以,不一定每次执行操作都要yield,尤其是当操作非常简单不需要多少时间更是如此。如果可能的话,可以采用每隔1000个执行才yield一次,能够显著增加性能。

       8. 测试和源代码下载

        用WPF写了一个DEMO,用了整整一个小时啊。 可通过点击界面下的按钮,添加和取消任务。在暂停CheckBox上勾选,可以随时暂停任务,取消勾选后,任务正常进行。

        可以添加多个任务1,但由于任务2需要写文件,因此只能生成一个任务2。

        需要安装.Net Framework 4.0,完整源代码

       

         时间仓促,有任何问题,随时讨论。

  • 相关阅读:
    UOJ #455 [UER #8]雪灾与外卖 (贪心、模拟费用流)
    Codeforces 482E ELCA (LCT)
    Codeforces 798D Mike and distribution (构造)
    AtCoder AGC017C Snuke and Spells
    HDU 6089 Rikka with Terrorist (线段树)
    HDU 6136 Death Podracing (堆)
    AtCoder AGC032D Rotation Sort (DP)
    jenkins+python+kubectl实现批量更新k8s镜像
    Linux 下载最新kubectl版本的命令:
    jenkins X 和k8s CI/CD
  • 原文地址:https://www.cnblogs.com/buptzym/p/4211768.html
Copyright © 2011-2022 走看看