概述
最近接到一個任務 要做一個《計划任務》的東西。簡而言之的說 就是事先設定好時間 定期執行指定代碼的功能 我們這個很簡單 就是每天或者每幾天 那天的一個固定時間比如23:20執行一段固定代碼,好,看一個界面
是不是很熟悉 哇哈哈哈,類似 Windows自帶的 計划任務功能。 對就是這個。按說的話微軟自家的東西 肯定有對應的接口或者 方便銜接的東西,於是網上找來找去 ,最終的結果還是沒有采用他,一個原因是Windows自帶的計划任務功能太復雜 我根本用不了那么多,第二個原因是沒有找到一個讓我舒心的調用方式。期間也找過其它的第三方的東西比如quartz 是很牛逼 看了下也是扯淡 大部頭的東西 又是要安裝這安裝那的 不適合我的簡易需求環境。
實現原理
后來靜下心來仔細想了下 憑什么到指定時間執行指定代碼 ,還不是程序開始的時候根據計划任務的時間進行了一個計算 然后設置一個timer讓代碼到期執行嗎,難道還有什么其他歪可以找嗎?搞清楚了事情的本質接下來就好辦了,就像期間找過一個mysql不能登錄的問題網上只說啥啥啥方式在命令行輸入 仔細細看一下那個圖上的語句明顯是一個mysql的控制台並不一定要按照他說的流程打開 有可能我沒添加環境變量各種原因打不開 我直接運行mysql控制台就好了, 所有做事情做不通前要想一下為很么 會好辦的多。計划任務本來不就是算過期時間嗎?什么你懷疑timer不穩定?不用timer用什么?按說時間計算是操作系統自暴露的一個API最基本的一個功能,就像文件讀取 就那樣了對於我們搞應用開發的 已經沒有更底層的可以挖了。就算你拿C++來 我相信還是只有用類似timer的這種玩意兒 不相信還有其他歪可以找。timer不穩定 不要使用winform界面的那個timer 那玩意兒估計確實不穩定 命名空間下有好幾個同名的東西。其實過程無法就是程序啟動的時候計算過期時間 按從設定之處開始算 接下來什么時候執行 ,一個timer 加計划任務數據 經過精密的邏輯編程即可解決所有問題 不用去搞任何第三方的東西 ,並且我還有可控的 銜接友好的 界面 進行 任務管理。
實現
其中最主要的是構建一個ScheduleItemObj對象,里面有任務開始時間 間隔天數 任務的當天執行時間 ,相信聰明如你 ,最重要的就是 里面有一個timer 然后 一個委托 委托里面是你要執行的代碼,然后就是初始化方法 初始化的時候進行邏輯計算 確定下次執行時間 設置timer ,當timer往后每次執行的時候自動計算下次時間 然后設置timer ,如此往復即可。多個任務放一個list里 通過propertyChange 進行界面更新,與管理。整個程序的結構大概就是這樣了。我們使用的是System.Timers.Timer
以下是實現代碼:注意最重要的一段代碼是計算下次任務執行時間邏輯的處理,執行時間的機制:從創建的當天 的指定時間 ,每過指定單位量時間執行計划任務,比如每天10:10:10 執行,如果創建的時候是09:00:00 則當天的 10:10:10 首次執行 往后以此類推。如果創建的時候是11:00:00 則在下一天的 10:10:10 首次執行 往后以此類推。配置一次就可以了 只要程序運行着 計划任務就會在指定日期,不論是中途手動退出程序重新開,計算機重啟,都無需干預 ,會自動按照計划任務列表里來執行,執行完后會自動刷新下次執行時間 和歷史執行記錄。代碼的注釋把以上原理闡述的很清楚。
void timer_Elapsed(object sender, ElapsedEventArgs e) { try { if (Started == false)//首次添加任務 或者程序剛啟動 安排接下來執行時間 { //進行首次的當天執行 DateTime now = DateTime.Now; DateTime actionTime; DateTimeFormatInfo dtfi = new CultureInfo("zh-CN", false).DateTimeFormat; bool convertok = DateTime.TryParseExact(Time, "HH:mm:ss", dtfi, DateTimeStyles.None, out actionTime); if (convertok == false) return; //年月日替換為 創建之日的 //開始時間必須從創建之時 開始算 switch (DaysUnit) { case "天"://如果躍遷為天 替換掉 從當天的天開始算(時分秒 以計划設定為准) actionTime = new DateTime(CreateAt.Year, CreateAt.Month, CreateAt.Day, actionTime.Hour, actionTime.Minute, actionTime.Second); break; case "時"://如果躍遷為時 替換掉 從當天的天.時開始算(分秒 以計划設定為准) actionTime = new DateTime(CreateAt.Year, CreateAt.Month, CreateAt.Day, CreateAt.Hour, actionTime.Minute, actionTime.Second); break; case "分"://如果躍遷為分 替換掉 從當天的天.時.分開始算(秒 以計划設定為准) actionTime = new DateTime(CreateAt.Year, CreateAt.Month, CreateAt.Day, CreateAt.Hour, CreateAt.Minute, actionTime.Second); break; default: break; } double interval = (actionTime - now).TotalMilliseconds; if ((actionTime - now).TotalMilliseconds <= 0)//計划時間在當前時間以前 { //設置下次執行時間 NextActionAt = actionTime; while (interval <= 0) { //如果很久沒有啟動程序了 加了躍遷 都還是在歷史日期 //一直加日期 直到加到超過當前日期 switch (DaysUnit) { case "天": actionTime = actionTime.AddDays(Days); break; case "時": actionTime = actionTime.AddHours(Days); break; case "分": actionTime = actionTime.AddMinutes(Days); break; default: break; } interval = (actionTime - now).TotalMilliseconds; } } //此處必定已經累加到interval躍遷大於0了 //Console.WriteLine("aaa"); if (interval > 0) { NextActionAt = actionTime; timer.Interval = interval; timer.AutoReset = false; timer.Start(); Console.WriteLine(ID + "初始化"); Started = true; } } else//運行中 { DateTime now = DateTime.Now; Console.WriteLine(ID + "執行於:" + NextActionAt.ToString()); try { //串口操作 ComDevice.Open(); byte[] data1 = new byte[] { 0xAA }; byte[] data2 = new byte[] { 0xBB }; byte[] data3 = new byte[] { 0xCC }; ComDevice.Write(data1, 0, 1); Thread.Sleep(Spand1 * 1000); ComDevice.Write(data2, 0, 1); Thread.Sleep(Spand2 * 1000); ComDevice.Write(data3, 0, 1); ComDevice.Close(); //操作完成 更新數據庫記錄 何下次action時間 ,並且設置自身nextActionAt if (historyAdd != null) { History hist = new History(); hist.Success = 1; hist.SuccessStr = "成功"; hist.ScheduleID = ID; hist.ActionAt = NextActionAt.Value; historyAdd.Invoke(hist); } } catch (Exception ex) { LoggerManager.Instance.WriteLog(ex.Message); if (historyAdd != null) { History hist = new History(); hist.Success = 0; hist.SuccessStr = "失敗"; hist.ScheduleID = ID; hist.ActionAt = NextActionAt.Value; historyAdd.Invoke(hist); } } finally { //無論如何都進行下次的計划任務定制 //執行到此處的時候肯定是actionAt時間到了,只需再加上days即可 switch (DaysUnit) { case "天": NextActionAt = NextActionAt.Value.AddDays(Days); break; case "時": NextActionAt = NextActionAt.Value.AddHours(Days); break; case "分": NextActionAt = NextActionAt.Value.AddMinutes(Days); break; default: break; } timer.Interval = (NextActionAt.Value - now).TotalMilliseconds; timer.AutoReset = false; timer.Start(); } } } catch (Exception ex) { Console.WriteLine("遇到錯誤:" + ex.Message); LoggerManager.Instance.WriteLog(ex.Message); } }
在程序啟動的時候從access數據庫讀取記錄 然后把所有計划任務都計算並啟動一遍。
public void StartSchedule() { //清理以前的 if (RunningSchedule != null) { for (int i = 0; i < RunningSchedule.Count; i++) { RunningSchedule[i].TimerClose(); } RunningSchedule.Clear(); } else { RunningSchedule = new ObservableCollection<ScheduleItemObj>(); } if (Data == null || Data.Count == 0) { return; } for (int i = 0; i < Data.Count; i++) { RunningSchedule.Add(Data[i]); Data[i].historyAdd += new Action<History>((hist) => { uiDispatcher.Invoke(new Action(() => { this.AddDBHistory(hist); this.AddHistoryVirtual(hist); })); }); Data[i].Start(); } }
當然 如果是添加新任務 我們也是很簡易粗暴的 添加數據 然后把所有任務停止,停止的時候會回收資源, 然后再啟動一遍 ,這樣便於我們更簡易的控制。
if (sw.ShowDialog() == true) { vm.AddSchedule(sw.day, sw.time.ToString("HH:mm:ss"),sw.daysUnit); vm.StartSchedule(); }
然后我們做了一個啟動時自動縮小到任務欄托盤運行的方式。我們是根據開始基礎日期 的設定進行 躍遷 單位 計算下次執行時間的 而不是累加,所以程序運行多久都不會出現執行時間上的偏差。
當然 有幾個東西要知曉 ,1應用程序必須要一直在運行期間計划任務才能夠得到成功執行,系統沒有登錄的情況下不會得到執行。我的運用環境是滿足的。 要規避這個問題網上說可以弄成服務形式的。
好了完工,上一個運行圖,好,完美,此程序現在已穩定運行相當長一段時間了,未出過問題。