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,完整源代码

       

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

  • 相关阅读:
    大作文-学以”成人”
    方案类--博物院整改意见
    归纳概括-我国中小学开展研学旅行活动的特点
    短文-网络新一代
    短评
    讲话稿-文明素养教育主题宣传
    检验用户单点登录方案解决
    Spring @Transactional注解
    RPC-局限于java的RMI
    Redis缓存雪崩、击穿、穿透的问题和解决方式
  • 原文地址:https://www.cnblogs.com/buptzym/p/4211768.html
Copyright © 2011-2022 走看看