記錄一次定時器報錯

孤沉發表於2024-05-16

報錯前因後果:
我現在使用Winform開發上位機程式,讀取PLC傳遞過來的CT,
1、我將定時器方法InitTimerTick();寫在構造器或者Load事件起作用
2、如果寫在後臺執行緒不起作用,也不報錯,我打斷點查詢的時候,發現InitCommonRegion方法沒有執行,我向上查詢,最終斷點打在 timer.Tick += new EventHandler(Timer_Tick);
3、如果後臺執行緒使用Task,那麼程式直接跳出去了,如果程式使用Thread,那麼VS2022直接卡死,注意我InitCommonRegion方法非UI執行緒的資料賦值給UI執行緒的控制元件已經使用了

this.Invoke(new Action(() => {
                         this.uiTextBox1.Text = result.ToString();
                     }));

我們最好將定時器寫在Load事件,防止控制元件還沒有初始化,定時器已經開始執行。
後臺執行緒不起作用不報錯的原因可能是因為InitCommonRegion()方法中使用了await和Task.Run,這會讓該方法變成非同步方法,呼叫InitCommonRegion()方法的執行緒會等待執行完成才繼續往下執行。而在後臺執行緒中,由於沒有訊息迴圈,非同步方法會導致執行緒無法執行完畢,從而無法更新UI介面,導致程式看起來沒有響應。而在除錯模式下,由於斷點的存在會暫停非同步方法的執行,從而讓後臺執行緒有機會執行InitCommonRegion()方法,並且在斷點處報錯。

至於為什麼使用Thread會將VS卡死,可能是因為在建立Thread時需要傳入ThreadStart委託,而該委託必須是一個無引數的方法,因此無法向該方法中傳遞引數,從而導致Thread無法呼叫InitCommonRegion()方法,從而陷入死迴圈,最終導致程式無響應。而使用Task則可以透過Task.Run的方式傳遞引數並啟動非同步方法。

建議使用非同步方法來讀取PLC資料,並且使用await避免程式阻塞,可以避免上述問題的發生。同時,在非同步方法中也可以使用Task.Delay方法來實現定時器的效果,從而避免使用Timer帶來的問題。
我最後的解決方案是取消了定時器的使用改為while true

  /// <summary>
  /// 工作執行緒
  /// </summary>
  /// <exception cref="NotImplementedException"></exception>
  private async void ExecuteWork()
  {
      while (!workMre.WaitOne(100)) 
      {
         await InitCommonRegion();
      }       
  }

問題程式碼如下 ,可以借鑑參考

public partial class Main_Form : UIForm
{
    IReadService _readService;
    IPlcService _plcService;
    IMesService _mesService;
    IServiceProvider _service;
    private ManualResetEvent helpMre = new ManualResetEvent(false);
    private ManualResetEvent workMre = new ManualResetEvent(false);
    private ManualResetEvent dataRefreshMre = new ManualResetEvent(false);
    public Main_Form(IReadService readService ,IPlcService plcService,IMesService mesService, IServiceProvider service)
    {
        _plcService = plcService;
        _mesService = mesService;
        _service = service;
        _readService = readService;

        InitializeComponent();
        this.Load += Main_Form_Load;
     //  InitTimerTick();
    }

    private void Main_Form_Load(object sender, System.EventArgs e)
    {
        Task workTask = new Task(ExecuteWork);
        workTask.Start();

        Task dataRefreshTask = new Task(ExecuteDataRefresh);
        dataRefreshTask.Start();

        Task helpTask = new Task(ExecuteHelper);
        helpTask.Start();
        // InitTimerTick();
        /* await Task.Run(() =>
         {
             InitTimerTick();
         });*/

        Thread thread = new Thread(Execute);
        thread.IsBackground = true;
        thread.Start();
    }

    private void Execute()
    {
        InitTimerTick();
    }

    /// <summary>
    /// 工作執行緒
    /// </summary>
    /// <exception cref="NotImplementedException"></exception>
    private void ExecuteWork()
    {
        while (!workMre.WaitOne(100)) 
        {
        } 
    }

    /// <summary>
    /// 資料傳遞執行緒
    /// </summary>
    /// <exception cref="NotImplementedException"></exception>
    private void ExecuteDataRefresh()
    {
        while (!dataRefreshMre.WaitOne(1000)) { }
    }

    /// <summary>
    /// 後臺輔助執行緒
    /// </summary>
    /// <exception cref="NotImplementedException"></exception>
    private void ExecuteHelper()
    {
        while (!helpMre.WaitOne(500)) { }
    }
    private void InitTimerTick()
    {
        // 建立Timer例項
        Timer timer = new Timer();
        // 設定觸發間隔時間,例如1秒
        timer.Interval = 500;
        // 訂閱Tick事件
        timer.Tick += new EventHandler(Timer_Tick);
        // 啟動Timer
        timer.Start();
    }

    private async Task InitCommonRegion()
    {
        try
        {
            await Task.Run(async () => {
                // 你的耗時PLC讀取操作
                var device01 = new Device01
                {
                    // ... 初始化 device01 ...
                    SlaveId=1
                };
                var result =await _plcService.ReadCoilsAsync(device01, 0, 5);//.Result;
                var s= (result.Result)as bool[] ;
              
                // 檢查是否有訪問UI執行緒的許可權
                if (this.uiTextBox1.InvokeRequired)
                {
                    // 使用 Invoke 確保在UI執行緒上更新控制元件
                    this.Invoke(new Action(() => {
                        this.uiTextBox1.Text = s[0].ToString();
                    }));
                }
                else
                {
                    // 如果已經在UI執行緒上,則直接更新控制元件
                    this.uiTextBox1.Text = "沒有資料";
                }
            });
        }
        catch (Exception e)
        {
            MessageBox.Show($"{e.Message}");
        }
    }

    private async void Timer_Tick(object sender, EventArgs e)
    {
       await  InitCommonRegion();              //使用定時器實時讀取公共區資料
    }

    private async void UpLoad_Load(object sender, System.EventArgs e)
    {
        // 假設這是在視窗載入時執行
        bool success = await _mesService.UploadDataToMesAsync(new UpLoadData());
        if (success)
        {
            MessageBox.Show("資料上傳成功");
        }
        else
        {
            MessageBox.Show("資料上傳失敗");
        }
    }

    // 確保在窗體關閉時設定_isRunning為false,以結束執行緒
    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        _isRunning = false; // 設定標誌,結束執行緒迴圈
        base.OnFormClosing(e);
       
    }
}

相關文章