通过IEnumerable和IDisposable实现可暂停和取消的任务队列
? ?一般來(lái)說(shuō),軟件中總會(huì)有一些長(zhǎng)時(shí)間的操作,這類操作包括下載文件,轉(zhuǎn)儲(chǔ)數(shù)據(jù)庫(kù),或者處理復(fù)雜的運(yùn)算。
一種處理做法是,在主界面上提示正在操作中,有進(jìn)度條,其他部分不可用。這里帶來(lái)很大的問題, 使用者不知道到底執(zhí)行到什么程度,無(wú)法暫停或者取消任務(wù)。而即使花了很大的力氣實(shí)現(xiàn)了暫停和取消,也很難形成通用的模塊。
另一種是類似下載工具那樣,有多個(gè)在任務(wù)隊(duì)列中的任務(wù),提示用戶當(dāng)前執(zhí)行了多少,可以選擇暫?;蛘呷∠蝿?wù)。如下圖:
顯然后者的用戶體驗(yàn)更好。那么,如何實(shí)現(xiàn)它呢?
應(yīng)當(dāng)考慮,這個(gè)任務(wù)管理器應(yīng)當(dāng)盡可能通用,作為基礎(chǔ)類庫(kù)為上層功能服務(wù),它應(yīng)該盡量友好,方便上層調(diào)用。
也許你已經(jīng)猜到了,關(guān)鍵就是枚舉器IEnumerable,更多可參考你可能不知道的陷阱, IEnumerable接口。
????? 由于時(shí)間倉(cāng)促,文章寫得比較粗略,感興趣研究的可以在文章末尾下載Demo查看。
1.可暫停的任務(wù)
首先,我們考慮如何實(shí)現(xiàn)暫停。主線程是不能暫停的,否則就無(wú)法響應(yīng)用戶操作,因此一定要有主線程之外的工作線程。為了方便,直接創(chuàng)建線程或使用線程池都比較麻煩,我們使用Task來(lái)創(chuàng)建新任務(wù)。
暫停一般有兩種做法,一種是信號(hào)量,一種是一個(gè)暫停標(biāo)記,不斷循環(huán)檢查標(biāo)記,否則就休眠一定時(shí)間。
顯然信號(hào)量更方便,消耗資源更少,而且無(wú)延遲。? 可以使用AutoResetEvent。我們先定義一個(gè)任務(wù)的基類:????
public abstract class TaskBase : PropertyChangeNotifier { //暫時(shí)省略了其他無(wú)關(guān)的代碼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; } }?
????? 在調(diào)用時(shí),可以使用類似以下的語(yǔ)句:
foreach (var task in tasks){CheckWait(); //如果IsPause被設(shè)置True,此處自動(dòng)阻塞action(task); //執(zhí)行對(duì)task的操作}?
2.取消
可以讓一個(gè)任務(wù)方便的啟動(dòng),但卻很難將其取消。強(qiáng)行終止工作線程,不僅可能不會(huì)立刻終止,同時(shí)還會(huì)引發(fā)異常,甚至造成不可預(yù)測(cè)的結(jié)果。所以我們采用盡可能優(yōu)雅的主動(dòng)檢測(cè)。
如何取消呢?可以使用CancellationTokenSource。 在每次枚舉過(guò)程中,檢查取消標(biāo)記,如果已經(jīng)取消,則break當(dāng)前枚舉。類似暫停的方法。
????? 具體代碼與暫停類似,可參考文章最后的Demo.
3.進(jìn)度條
實(shí)現(xiàn)進(jìn)度是比較容易也是困難的事情,要知道整個(gè)枚舉的數(shù)量,通過(guò)外部數(shù)據(jù)來(lái)提示它。傳入一個(gè)當(dāng)前的位置,求出與整個(gè)位置的比值,即可得到進(jìn)度。
4.多個(gè)任務(wù)的任務(wù)隊(duì)列
我們期望能夠形成任務(wù)隊(duì)列,這些任務(wù)可以調(diào)整執(zhí)行順序,還能夠順次或同時(shí)執(zhí)行,根據(jù)以上的知識(shí),就可以構(gòu)造下面的類出來(lái):
/// <summary>/// 任務(wù)調(diào)度器/// </summary>public class BatchTaskScheduler{public BatchTaskScheduler() { CurrentProcessTasks=new ObservableCollection<TaskBase>(); } /// <summary> /// 當(dāng)前所有執(zhí)行的任務(wù) /// </summary> public IList<TaskBase> CurrentProcessTasks { get; set; } /// <summary> /// 添加一個(gè)臨時(shí)任務(wù) /// </summary> /// <typeparam name="T"></typeparam> /// <param name="taskName">任務(wù)名稱</param> /// <param name="enumable">任務(wù)枚舉器</param> /// <param name="action">對(duì)枚舉的每一個(gè)元素執(zhí)行的操作</param> /// <param name="contineAction">枚舉完成后執(zhí)行的操作</param> /// <param name="count">可選,枚舉總數(shù)量,用于指示進(jìn)度條</param> /// <param name="autoStart">是否自動(dòng)運(yùn)行</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; //此處可能能夠獲取整個(gè)枚舉的大小 } 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; //計(jì)算進(jìn)度條位置 } 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(); } } }?
使用起來(lái)也很方便:
public IEnumerable<int> TestTask(int count) //表達(dá)一個(gè)耗時(shí)的函數(shù) {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("任務(wù)1:延時(shí)測(cè)試", TestTask(total)); Scheduler.AddTempTask("任務(wù)1:延時(shí)測(cè)試", TestTask(total), null, result => MessageBox.Show(string.Format("延時(shí)測(cè)試任務(wù)已經(jīng)完成,迭代位置{0}", result)), total);
}
?? 當(dāng)然,你可以將TestTask函數(shù)換成自己的匿名函數(shù)。
5.改造已有的耗時(shí)代碼(不安全)
這些耗時(shí)代碼可能是在類庫(kù)中已經(jīng)存在的大量代碼,那么,如何能夠盡可能方便地修改它們,以適合以上的模式呢?還是枚舉器,yield return.
以寫入文件為例,說(shuō)明如何改造:
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); //模擬耗時(shí)任務(wù) sw.WriteLine("這個(gè)數(shù)據(jù)是" + j); yield return j; }sw.Close(); fs.Close();
yield return j; }
?
值得注意的是,這段代碼并不會(huì)主動(dòng)執(zhí)行,由于引入了yield,它的執(zhí)行需要外部去“推”。因此一個(gè)很有可能發(fā)生的問題是,如果不去檢查返回值,那么這段代碼就不會(huì)執(zhí)行!這個(gè)確實(shí)是違反直覺的。外界用多少就執(zhí)行多少。
???? 如果想對(duì)其全部執(zhí)行,可以使用var r= WriteFileUnsafe(filename,100).LastOrDefault(); 這個(gè)方法會(huì)將枚舉推到最后一步。但是,r不使用的話,會(huì)不會(huì)被編譯器優(yōu)化掉呢?
???? ???? 細(xì)心的讀者可能會(huì)發(fā)現(xiàn),上面的代碼 是不安全的,因?yàn)橐肓藋ield,所以try-catch變得雞肋。同時(shí),一旦用戶取消了這個(gè)操作,其實(shí)資源是沒有被回收的!這段代碼會(huì)在某一次yield之后直接返回,這會(huì)造成嚴(yán)重的安全問題!?
???? 可能有人會(huì)想到,通過(guò)外界判斷是否執(zhí)行完畢,傳入委托告訴調(diào)度器如何回收資源。可是,這破壞了代碼的一致性。如何回收資源應(yīng)當(dāng)是使用資源本身的函數(shù)所考慮的,而不應(yīng)該交給其他類。
6. 使用IDisposable模式解決安全問題
????? 為了保證在隨時(shí)取消任務(wù)之后,回收資源的代碼被執(zhí)行,所以必須考慮特別的方法。
????? Try-catch代碼塊是不用想了,因?yàn)槊杜eyield中是不支持的,不能通過(guò)拋異常來(lái)解決。
????? 那么就引入using吧,使用IDisposable模式! 定義一個(gè)輔助類:
public class DisposeHelper : IDisposable{private Action action;public DisposeHelper(Action action2) { action = action2; } public void Dispose() { action(); } }?
這個(gè)類非常簡(jiǎn)單,只有一個(gè)委托。在Dispose的時(shí)候調(diào)用該委托執(zhí)行操作。使用起來(lái)更是碉堡了:
????
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(); //不論是拋出異常,還是取消任務(wù),還是正常完成,這段代碼一定會(huì)被執(zhí)行 })) { for (j = 0; j < count; j++) { Thread.Sleep(100); sw.WriteLine("這個(gè)數(shù)據(jù)是" + j); yield return j; } } }?????? 我們狠狠的舔了一下using這個(gè)語(yǔ)法糖。
?????? 基本上有了這些之后,功能就比較全面了。下面上測(cè)試樣例:
private void Button_Click_2(object sender, RoutedEventArgs e){string fileName = "Test.txt";Scheduler.AddTempTask("任務(wù)2:寫入文件", WriteFileTask(fileName, 100), null, d => { Process.Start(fileName); }, 100); }? ?7.性能問題
使用這種模式之后,我發(fā)現(xiàn),它在做一些操作的時(shí)候,會(huì)比不使用這種模式來(lái)的更慢一些。其原因可能有這么幾點(diǎn):
(1)修改了CurrentIndex值,而該值通過(guò)屬性通知方法,不斷的通知UI,可能會(huì)造成性能損失
(2)yield模式降低了代碼的命中率,使得CPU的跳轉(zhuǎn)大大增加。
所以,不一定每次執(zhí)行操作都要yield,尤其是當(dāng)操作非常簡(jiǎn)單不需要多少時(shí)間更是如此。如果可能的話,可以采用每隔1000個(gè)執(zhí)行才yield一次,能夠顯著增加性能。
?? 8. 測(cè)試和源代碼下載
??? 用WPF寫了一個(gè)DEMO,用了整整一個(gè)小時(shí)啊。 可通過(guò)點(diǎn)擊界面下的按鈕,添加和取消任務(wù)。在暫停CheckBox上勾選,可以隨時(shí)暫停任務(wù),取消勾選后,任務(wù)正常進(jìn)行。
??? 可以添加多個(gè)任務(wù)1,但由于任務(wù)2需要寫文件,因此只能生成一個(gè)任務(wù)2。
??? 需要安裝.Net Framework 4.0,完整源代碼。
???
???? 時(shí)間倉(cāng)促,有任何問題,隨時(shí)討論。
作者:熱情的沙漠
出處:http://www.cnblogs.com/buptzym/
本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。
本文轉(zhuǎn)自FerventDesert博客園博客,原文鏈接:http://www.cnblogs.com/buptzym/p/4211768.html,如需轉(zhuǎn)載請(qǐng)自行聯(lián)系原作者
與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖
總結(jié)
以上是生活随笔為你收集整理的通过IEnumerable和IDisposable实现可暂停和取消的任务队列的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Matplotlib 中文用户指南 4.
- 下一篇: Angular面试题三