如何在 ElapsedEvent 期间保持 System.Timers.Timer 停止

Posted

技术标签:

【中文标题】如何在 ElapsedEvent 期间保持 System.Timers.Timer 停止【英文标题】:How to keep a System.Timers.Timer stopped during ElapsedEvent 【发布时间】:2021-07-23 13:26:10 【问题描述】:

我正在使用 System.Timers.Timer 并且每隔 x 秒我需要在 ElapsedEvent 方法中执行一些任务。当我在 ElapsedEvent 方法中执行任务时,我希望停止计时器。但是,我有另一种可以启动计时器的方法,可以在 ElapsedEvent 运行时调用它。我的代码如下所示:

class MyClass 
   Timer myTimer;

   public MyClass 
      myTimer = new System.Timers.Timer();
      // init timer code here...
   

   public void ElapsedEventTask(object source, ElapsedEventArgs e) 
      myTimer.Enabled = false;
      try
      
          // do my tasks
      
      catch
      
         ...
      
      finally
      
         myTimer.Enabled = true;
      
   


public void AnotherMethod() 
   // do some things
   myTimer.Enabled = true;

当我完成ElapsedEventTask 中的任务时,如何防止AnotherMethod 启动计时器?

【问题讨论】:

AnotherMethod方法打算只调用一次,第一次启动计时器,还是你还有一个停止计时器的方法,这样可以手动启动和停止计时器多次? AnotherMethod 可以多次调用,还有另一种方法可以停止定时器。所以是的,myTimer 可以手动启动和停止多次。 【参考方案1】:

您可以添加一个变量来指示任务是否正在运行。最后为了线程安全,当这个变量与myTimer.Enabled一起使用时,你需要使用lock

class MyClass

    object syncEnableRunning = new object();
    bool running
    Timer myTimer;

    public void ElapsedEventTask(object source, ElapsedEventArgs e)
    
        lock(syncEnableRunning)
        
            running = true;
            myTimer.Enabled = false;
        
        
        try  /*do my tasks*/
        catch  ... 
        finally
        
            lock(syncEnableRunning)
            
                myTimer.Enabled = true;
                running = false;
            
        
    

    public void AnotherMethod()
    
        // do some things
        lock(syncEnableRunning)
        
            if(!running)
            
                myTimer.Enabled = true;
            
        
    

【讨论】:

我认为volatile 的使用不足以保证这里的线程安全。有可能在线程 B 上的 Elapsed 事件滴答声之前,线程 A 上会调用方法 AnotherMethod,但是在 AnotherMethod 调用期间会发生 OS 级别的线程切换,并且线程 A 将执行同一属性之后的myTimer.Enabled = true; 行已由线程B 设置为false。为了使其线程安全,您需要引入适当的lock 恕我直言。 @TheodorZoulias,完全同意……真丢脸。感谢您的建议,我编辑了答案。【参考方案2】:

根据documentation,System.Timers.Timer 类不是线程安全的,因此在没有同步的情况下从多个线程触摸其Enabled 属性是不安全的(这样做会导致未定义的行为)。 Vernou 的answer 展示了如何使用锁来同步线程,但我个人对尝试使用显然被设计为可重入的机制强制执行非重叠执行策略感到有些紧张。所以我的建议是放弃System.Timers.Timer,改用异步循环,由Stephen Cleary 的PauseTokenSource 机制控制:

class MyClass

    private readonly CancellationTokenSource _cts;
    private readonly PauseTokenSource _pts;
    public Task Completion  get; private set; 

    public MyClass(TimeSpan interval)
    
        _cts = new CancellationTokenSource();
        _pts = new PauseTokenSource();
        _pts.IsPaused = true;
        Completion = Task.Run(async () =>
        
            try
            
                while (true)
                
                    await _pts.Token.WaitWhilePausedAsync(_cts.Token);
                    var delayTask = Task.Delay(interval, _cts.Token);
                    /* Do my tasks */
                    await delayTask;
                
            
            catch (OperationCanceledException)
                when (_cts.IsCancellationRequested)   // Ignore
        );
    

    public void Start() => _pts.IsPaused = false;
    public void Stop() => _pts.IsPaused = true;
    public void Complete() => _cts.Cancel();

PauseTokenSourcePauseToken 的控制器,与 CancellationTokenSource/CancellationToken 组合的概念类似。区别在于CancellationTokenSource 只能取消一次,而PauseTokenSource 可以多次暂停/取消暂停。这个类包含在AsyncEx.Coordination 包中。

MyClass 公开了一个终止异步循环的Complete 方法,以及一个可以等待的Completion 属性。在关闭程序之前await 这个属性是个好主意,让任何活动操作都有机会完成。否则进程可能会在后台执行过程中被杀死,后果无法预料。

【讨论】:

这看起来比它需要的要复杂得多。多线程的概念从何而来? OP 使用单线程,问题是他不希望计时器在第一次完成之前第二次打勾。他的示例中没有涉及多线程/任务。 @Neil 这是一个由令牌控制的异步循环。循环以明显的方式提供了 OP 正在搜索的非重叠保证。两个连续的迭代并行运行在逻辑上是不可能的。每次迭代可能在不同的线程上运行,但不会在前一次迭代完成之前开始。 await 点可能会发生线程切换,但这不是强制性的。如果ThreadPool 的需求较低并且只创建了一个线程,则所有迭代都将在同一线程上运行。 @Neil 关于这个解决方案过于复杂,如果有替代解决方案可用,这将是一个公平的论点,它提供了相同的行为保证,但不那么复杂。 Vernou 的solution 可能 产生理想的行为,但如果不先彻底研究System.Timers.Timer 类的源代码并说服自己即使在扩展ThreadPool饱和的极端条件。恕我直言,这要复杂得多。【参考方案3】:

我会创建一个一次性计时器,然后您需要在计时器功能结束时重新启动它。

myTimer = new System.Timers.Timer();
myTimer.AutoReset = false;


public void ElapsedEventTask(object source, ElapsedEventArgs e) 
  ...
  finally
  
     myTimer.Start();
  

【讨论】:

以上是关于如何在 ElapsedEvent 期间保持 System.Timers.Timer 停止的主要内容,如果未能解决你的问题,请参考以下文章

如何在部署期间保持数据同步?

如何在文件上传期间保持页面可编辑(Laravel)?

dbms 如何在事务期间保持原子性?

如何修复确保文本在 webfont 加载期间保持可见

如何在“每次”迭代期间附加列表时保持列表的长度不变

为啥 UITableView contentOffset 在 tableview reloadData 期间变化很大,而不是保持不变? reloadData 后如何使其保持不变?