【问题标题】:Single threaded timer单线程定时器
【发布时间】:2011-03-31 09:04:39
【问题描述】:

我想要一个具有以下属性的计时器:

  1. 无论调用多少次start,只有一个回调线程一直在运行

  2. 关于时间间隔,在回调函数中花费的时间被忽略了。例如,如果间隔为 100 毫秒,并且回调需要 4000 毫秒执行,则在 100 毫秒、4100 毫秒等处调用回调。

我看不到任何可用的内容,因此编写了以下代码。有没有更好的方法来做到这一点?

/**
 * Will ensure that only one thread is ever in the callback
 */
public class SingleThreadedTimer : Timer
{
    protected static readonly object InstanceLock = new object();
    
    //used to check whether timer has been disposed while in call back
    protected bool running = false;

    virtual new public void Start()
    {
        lock (InstanceLock)
        {
            this.AutoReset = false;
            this.Elapsed -= new ElapsedEventHandler(SingleThreadedTimer_Elapsed);
            this.Elapsed += new ElapsedEventHandler(SingleThreadedTimer_Elapsed);
            this.running = true;
            base.Start();
        }
        
    }

    virtual public void SingleThreadedTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
        lock (InstanceLock)
        {
            DoSomethingCool();

            //check if stopped while we were waiting for the lock,
            //we don't want to restart if this is the case..
            if (running)
            {
                this.Start();
            }
        }
    }

    virtual new public void Stop()
    {
        lock (InstanceLock)
        {
            running = false;
            base.Stop();
        }
    }
}

【问题讨论】:

标签: c# multithreading timer


【解决方案1】:

这是我刚刚敲出的一个简单示例;

using System.Threading;
//...
public class TimerExample
{
    private System.Threading.Timer m_objTimer;
    private bool m_blnStarted;
    private readonly int m_intTickMs = 1000;
    private object m_objLockObject = new object();

    public TimerExample()
    {
        //Create your timer object, but don't start anything yet
        m_objTimer = new System.Threading.Timer(callback, m_objTimer, Timeout.Infinite, Timeout.Infinite);
    }

    public void Start()
    {
        if (!m_blnStarted)
        {
            lock (m_objLockObject)
            {
                if (!m_blnStarted) //double check after lock to be thread safe
                {
                    m_blnStarted = true;

                    //Make it start in 'm_intTickMs' milliseconds, 
                    //but don't auto callback when it's done (Timeout.Infinite)
                    m_objTimer.Change(m_intTickMs, Timeout.Infinite);
                }
            }
        }
    }

    public void Stop()
    {
        lock (m_objLockObject)
        {
            m_blnStarted = false;
        }
    }

    private void callback(object state)
    {
        System.Diagnostics.Debug.WriteLine("callback invoked");

        //TODO: your code here
        Thread.Sleep(4000);

        //When your code has finished running, wait 'm_intTickMs' milliseconds
        //and call the callback method again, 
        //but don't auto callback (Timeout.Infinite)
        m_objTimer.Change(m_intTickMs, Timeout.Infinite);
    }
}

【讨论】:

  • 很好的答案,谢谢。我在想如果两个线程同时调用 Start 例程会发生什么?
  • @Richard 公平点,答案仅适用于单线程应用程序。我已经编辑了我的答案,以了解锁定将在何处发挥作用。如果您愿意,您可以随时锁定(this)。
  • @Richard:为了确保在给定时间只有一个线程会调用启动例程,您可以使用 Mutex。更多细节可以在这里找到msdn.microsoft.com/en-us/library/ms173179.aspx
【解决方案2】:

适用于任何需要单线程计时器并希望计时器在 任务完成后开始计时的人。 System.Timers.Timer 可以在没有锁定或 [ThreadStatic] 的情况下完成任务

System.Timers.Timer tmr;

void InitTimer(){
    tmr = new System.Timers.Timer();
    tmr.Interval = 300;
    tmr.AutoReset = false;
    tmr.Elapsed += OnElapsed;
}

void OnElapsed( object sender, System.Timers.ElapsedEventArgs e )
{
    backgroundWorking();

    // let timer start ticking
    tmr.Enabled = true;
}

归功于Alan N 来源https://www.codeproject.com/Answers/405715/System-Timers-Timer-single-threaded-usage#answer2

编辑:间距

【讨论】:

    【解决方案3】:

    .NET Framework 提供 四个 计时器。其中两个是通用多线程 计时器:

    • System.Threading.Timer
    • System.Timers.Timer

    另外两个是专用的单线程定时器

    • System.Windows.Forms.Timer(Windows 窗体计时器)
    • System.Windows.Threading.DispatcherTimer(WPF 计时器)

    最后 2 个旨在消除 WPF 和 Windows 窗体应用程序的线程安全问题

    例如,在计时器内使用 WebBrowser 从网页中捕获屏幕截图需要是单线程的,如果它在另一个线程上,则会在运行时出错。

    单线程定时器有以下好处

    • 您可以忘记线程安全。
    • 在前一个 Tick 完成之前,永远不会触发新的 Tick 处理。
    • 您可以直接从 勾选事件处理代码,无需调用 Control.BeginInvoke 或 Dispatcher.BeginIn 调用。

    和主要缺点要注意

    • 一个线程服务于所有定时器,以及处理 UI 事件。 这意味着 Tick 事件处理程序必须快速执行, 否则用户界面将无响应。

    source: 大部分是C# in a Nutshell 书 -> 第 22 章 -> 高级线程 -> 定时器 -> 单线程定时器

    【讨论】:

      【解决方案4】:

      查看[ThreadStatic] 属性和.Net 4.0 ThreadLocal 泛型类型。这可能会很快为您提供一种编写代码的方法,而不会弄乱线程锁定等。

      你可以在你的时间类中有一个堆栈,你可以实现一个返回 IDisposable 的 Monitor() 方法,所以你可以像这样使用计时器:

      using (_threadTimer.Monitor())
      {
           // do stuff
      }
      

      让计时器监视器在 Dispose() 期间将间隔时间戳从堆栈中弹出。

      如前所述,手动编码所有锁定和线程识别是一种选择。但是,锁定会影响使用的时间,很可能不仅仅是必须使用 ThreadLocal 为每个线程初始化一个实例

      如果你有兴趣,我稍后可能会举一个例子

      【讨论】:

        【解决方案5】:

        这是一个简单的PeriodicNonOverlappingTimer 类,它只提供所要求的功能,仅此而已。此计时器不能按需启动和停止,也不能更改其间隔。它只是以非重叠的方式周期性地调用指定的动作,直到定时器被释放。

        /// <summary>
        /// Invokes an action on the ThreadPool at specified intervals, ensuring
        /// that the invocations will not overlap, until the timer is disposed.
        /// </summary>
        public class PeriodicNonOverlappingTimer : IDisposable, IAsyncDisposable
        {
            private readonly System.Threading.Timer _timer;
        
            public PeriodicNonOverlappingTimer(Action periodicAction,
                TimeSpan dueTime, TimeSpan period)
            {
                // Arguments validation omitted
                _timer = new(_ =>
                {
                    var stopwatch = Stopwatch.StartNew();
                    periodicAction();
                    var nextDueTime = period - stopwatch.Elapsed;
                    if (nextDueTime < TimeSpan.Zero) nextDueTime = TimeSpan.Zero;
                    try { _timer.Change(nextDueTime, Timeout.InfiniteTimeSpan); }
                    catch (ObjectDisposedException) { } // Ignore this exception
                });
                _timer.Change(dueTime, Timeout.InfiniteTimeSpan);
            }
        
            public void Dispose() => _timer.DisposeAsync().AsTask().Wait();
            public ValueTask DisposeAsync() => _timer.DisposeAsync();
        }
        

        使用示例。演示如何创建一个立即启动的非重叠计时器,周期为 10 秒。

        var timer = new PeriodicNonOverlappingTimer(() =>
        {
            DoSomethingCool();
        }, TimeSpan.Zero, TimeSpan.FromSeconds(10));
        
        //...
        
        timer.Dispose(); // Stop the timer once and for all
        

        如果DoSomethingCool失败,则会在ThreadPool上抛出异常,导致进程崩溃。所以你可能想添加一个try/catch 块,并处理所有可能发生的异常。

        Dispose 是一种潜在的阻塞方法。如果periodicAction 当前正在运行,Dispose 将阻塞直到最后一次调用完成。 如果您不想等待这种情况发生,您可以这样做:

        _ = timer.DisposeAsync(); // Stop the timer without waiting it to finish
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-02-25
          • 2011-03-04
          • 2021-12-03
          • 2011-10-18
          • 2011-05-21
          相关资源
          最近更新 更多