實現一個“計劃任務”機制

assassinx發表於2020-10-11

概述

最近接到一個任務 要做一個《計劃任務》的東西。簡而言之的說 就是事先設定好時間 定期執行指定程式碼的功能 我們這個很簡單 就是每天或者每幾天 那天的一個固定時間比如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應用程式必須要一直在執行期間計劃任務才能夠得到成功執行,系統沒有登入的情況下不會得到執行。我的運用環境是滿足的。 要規避這個問題網上說可以弄成服務形式的。

好了完工,上一個執行圖,好,完美,此程式現在已穩定執行相當長一段時間了,未出過問題。

 

 

 

 

 

 

相關文章