通过IEnumerable和IDisposable实现可暂停和取消的任务队列
? ?一般來說,軟件中總會有一些長時間的操作,這類操作包括下載文件,轉儲數據庫,或者處理復雜的運算。
一種處理做法是,在主界面上提示正在操作中,有進度條,其他部分不可用。這里帶來很大的問題, 使用者不知道到底執行到什么程度,無法暫?;蛘呷∠蝿铡6词够撕艽蟮牧鈱崿F了暫停和取消,也很難形成通用的模塊。
另一種是類似下載工具那樣,有多個在任務隊列中的任務,提示用戶當前執行了多少,可以選擇暫停或者取消任務。如下圖:
顯然后者的用戶體驗更好。那么,如何實現它呢?
應當考慮,這個任務管理器應當盡可能通用,作為基礎類庫為上層功能服務,它應該盡量友好,方便上層調用。
也許你已經猜到了,關鍵就是枚舉器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,如需轉載請自行聯系原作者
與50位技術專家面對面20年技術見證,附贈技術全景圖
總結
以上是生活随笔為你收集整理的通过IEnumerable和IDisposable实现可暂停和取消的任务队列的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Matplotlib 中文用户指南 4.
- 下一篇: Angular面试题三