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

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

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

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

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

  也许你已经猜到了,关键就是枚举器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,完整源代码。

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

作者:热情的沙漠
出处:http://www.cnblogs.com/buptzym/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

分类: 算法
本文转自FerventDesert博客园博客,原文链接:http://www.cnblogs.com/buptzym/p/4211768.html,如需转载请自行联系原作者

通过IEnumerable和IDisposable实现可暂停和取消的任务队列相关推荐

  1. IOS开发教程第一季之03多线程day3--最大并发数,队列的暂停,取消和恢复、操作优先级、线程监听、多线程下UITableView显示图片案例

    1.NSOperation–最大并发数 什么是并发数 同时执行的任务数,比如同时开3个线程执行3个任务,并发数就是3 最大并发数的相关方法 -(NSInteger)maxConcurrentOpera ...

  2. C# Task 暂停与取消

    前言: ①取消task任务之CancellationTokenSource的用法: ②task的线程管控方法Task..Wait(time),Task.WaitAll(), Task.WaitAny( ...

  3. 计算机正在在更新怎么停止运行,Win10系统更新到一半该怎么暂停或取消呢?

    Haley 于 2020/07/14更新 备份&还原 摘要 你知道怎么在Windows更新执行后取消更新吗?仔细阅读下文,你就会知道如何终止执行中的Windows 10 更新了.如果你想以最安 ...

  4. 耳机声控,以及耳机拔出或者插入控制播放暂停

    目录介绍 1.耳机拔出时暂停播放 1.1 拔出耳机自动暂停 , 插入耳机自动恢复播放 1.2 实现的原理分析 1.3 代码实现逻辑 2.耳机线控,耳机控制声音 2.1 耳机按键也可以控制音量调节 2. ...

  5. 深入理解操作系统(12)第四章:处理器体系结构(4)Y86-64的流水线实现(包括:PIPE-处理器/预测下一个PC/分支预测/流水线冒险/暂停,转发避免冒险/PPE硬件结构及实现/CPI)

    深入理解操作系统(12)第四章:处理器体系结构(4)Y86-64的流水线实现(包括:PIPE-处理器/预测下一个PC/分支预测/流水线冒险/暂停,转发避免冒险/PPE硬件结构及实现/CPI) 1. Y ...

  6. android如何暂停倒计时,Android – 如何停止和暂停计时器

    我一直在经历很多问题尝试暂停和取消暂停计时器,如果我将方向锁定为纵向或横向它可以工作,但这不完全是我想要做的.当然,当您更改方向时会调用onCreate方法,因此我取消了我的timertask并将其设 ...

  7. OKHttp3实现文件下载,断点下载,暂停下载

    OKHttp3是如今非常流行的Android网络请求框架,那么如何利用Android实现断点续传呢,今天写了个Demo尝试了一下,感觉还是有点意思 代码地址 http://download.csdn. ...

  8. C# TaskScheduler

    这里记录下 TaskScheduler 的简单用法. 使用场景: 使用 Task 的时候,大家知道用 TaskFactory.StartNew 可以用来创建一个 Task .这里如果创建了 3 个,那 ...

  9. .NET新手系列(六)

    一.遇到的问题: 关于密码体系的初步知识 解决方法: 1对称加密算法 在计算机专网系统中广泛使用的对称加密算法有DES和IDEA等.最初的原理包括字母的换位.替换. 2不对称加密算法 不对称加密算法有 ...

最新文章

  1. 手把手教你在应用里用上iOS机器学习框架Core ML
  2. 0x17.基础数据结构 - 二叉堆
  3. linux下查找某个文件位置的方法
  4. 模拟退火求函数最值问题求解
  5. 2020年度国家绿色数据中心名单正式发布
  6. 蓝桥杯-字串统计(java)
  7. Linux下高性能网络编程中的几个TCP/IP选项_SO_REUSEADDR、SO_RECVBUF、SO_SNDBUF、SO_KEEPALIVE、SO_LINGER、TCP_CORK、TCP_NODE
  8. raid配置ssd为缓存_超融合硬件选配推荐指南 | 第三期:SSD 与 HDD
  9. 早晚安打卡签到v2.0.1 公众号模块
  10. OpenCV总结:实现马赛克和毛玻璃滤镜效果
  11. SQL Plus的使用详解(登录和常用命令)
  12. c语言求最小公倍数——三种方法
  13. 圆弧周长公式_弧长的计算公式是什么?
  14. pytorch 基本数学运算
  15. 蓝桥杯 算法训练 跳马
  16. 漫画安全HIDS、EDR、NDR、XDR
  17. 名帖59 褚遂良 小楷《千字文》
  18. 黑马培训学编程python
  19. 三维渲染 光能辐射基础
  20. 4.20 视频面试字节_光大银行_神策

热门文章

  1. 如何搭建一个简易的Web框架
  2. Selenium Webdriver概述(转)
  3. apps-privacy-policy
  4. 20172332 2017-2018-2 《程序设计与数据结构》实验三报告
  5. 爬虫图谱(个人整理使用)
  6. 洛谷【p2817】 宋荣子的城堡
  7. 制作bat脚本,抓取Android设备logcat
  8. 求二维数组的子数组中的最大值!
  9. windows API函数copyfile
  10. 最新版Kubernetes常用命令大全