非重入C#计时器

时间:2010-06-06 19:45:30

标签: c# multithreading timer

我正在尝试每f()次调用一个方法t,但如果先前的f()调用尚未完成,请等到它完成。

我已经阅读了一些关于可用计时器的内容,但找不到任何好的方法来做我想要的,除了手动编写所有内容。关于如何实现这一目标的任何帮助都将受到赞赏,但我担心我可能无法使用计时器找到一个简单的解决方案。

澄清一下,如果t是一秒,而f()运行我在下面写的任意持续时间,那么:

Step  Operation    Time taken
1     wait         1s
2     f()          0.6s
3     wait         0.4s (because f already took 0.6 seconds)
4     f()          10s
5     wait         0s (we're late)
6     f()          0.3s
7     wait         0.7s (we can disregard the debt from step 4)

请注意,此计时器的性质是f()在重新进入时不需要是安全的,并且此处的大小为1的线程池就足够了。

8 个答案:

答案 0 :(得分:12)

使用System.Threading.Timer。用一段Timeout.Infinite初始化它,所以它就像一次性计时器。当f()完成时,调用其Change()方法再次为其充电。

答案 1 :(得分:8)

您可以使用“全局”级别var(或者更可能是与f()在同一个类中的公共属性),如果f()已在运行,则返回true。

因此,如果f()位于名为TimedEvent的类中,f()要做的第一件事就是设置Running为真

这样你的计时器每秒都会触发,然后启动定时事件(如果它还没有运行) if (!timedEvent.Running) timedEvent.f()

您评论说f()如果花费的时间超过计时器间隔,则不会立即重复。这是一个公平的观点。我可能会在f()内包含类似逻辑,以便Running保持为真。所以它看起来像这样:

public void f(int t) // t is interval in seconds
{
   this.running = true;

   Stopwatch stopWatch = new Stopwatch();
   stopWatch.Start();

   do
   {
       stopwatch.Reset();

       // Do work here

   } while (stopWatch.Elapsed.Seconds > t); // repeat if f() took longer than t

   this.running = false;   
}

答案 2 :(得分:3)

您可以使用非重启计时器,然后在方法完成后手动重启计时器。

请注意,这会导致时间与您要求的时间略有不同。 (调用之间总是存在t时间间隔)

您可以通过将时间间隔设置为lastTick + t - Now来解决此问题,并在<= 0时立即运行该方法。

如果您需要停止计时器,请注意竞争条件。

答案 3 :(得分:1)

你无法让计时器按计划的时间间隔给你打电话。所有计时器都会在所要求的时间之前给你回电话。

某些计时器比其他计时器更好(例如,与System.Threading.Timer相比,Windows.Forms.Timer非常不稳定且不可靠)

要停止重新调用计时器,一种方法是在方法运行时停止计时器。 (根据您使用的计时器类型,您可以停止它并在处理程序退出时再次启动它,或者使用某些计时器可以请求单个回调而不是重复回调,因此每次执行处理程序只会将下一个调用排入队列)

为了在这些调用之间保持相对均匀的时间,您可以记录自上次执行处理程序以来的时间,并使用它来计算延迟,直到需要下一个事件。例如如果你想每秒调用一次并且你的计时器在1.02s完成,那么你可以设置下一个计时器回调,持续时间为0.98秒,以适应你已经“用完”下一部分的事实在您处理期间的第二个。

答案 4 :(得分:0)

一个简单的解决方案:

private class Worker : IDisposable
{
    private readonly TimeSpan _interval;
    private WorkerContext _workerContext;

    private sealed class WorkerContext
    {
        private readonly ManualResetEvent _evExit;
        private readonly Thread _thread;
        private readonly TimeSpan _interval;

        public WorkerContext(ParameterizedThreadStart threadProc, TimeSpan interval)
        {
            _evExit = new ManualResetEvent(false);
            _thread = new Thread(threadProc);
            _interval = interval;
        }

        public ManualResetEvent ExitEvent
        {
            get { return _evExit; }
        }

        public TimeSpan Interval
        {
            get { return _interval; }
        }

        public void Run()
        {
            _thread.Start(this);
        }

        public void Stop()
        {
            _evExit.Set();
        }

        public void StopAndWait()
        {
            _evExit.Set();
            _thread.Join();
        }
    }

    ~Worker()
    {
        Stop();
    }

    public Worker(TimeSpan interval)
    {
        _interval = interval;
    }

    public TimeSpan Interval
    {
        get { return _interval; }
    }

    private void DoWork()
    {
        /* do your work here */
    }

    public void Start()
    {
        var context = new WorkerContext(WorkThreadProc, _interval);
        if(Interlocked.CompareExchange<WorkerContext>(ref _workerContext, context, null) == null)
        {
            context.Run();
        }
        else
        {
            context.ExitEvent.Close();
            throw new InvalidOperationException("Working alredy.");
        }
    }

    public void Stop()
    {
        var context = Interlocked.Exchange<WorkerContext>(ref _workerContext, null);
        if(context != null)
        {
            context.Stop();
        }
    }

    private void WorkThreadProc(object p)
    {
        var context = (WorkerContext)p;
        // you can use whatever time-measurement mechanism you want
        var sw = new System.Diagnostics.Stopwatch();
        int sleep = (int)context.Interval.TotalMilliseconds;
        while(true)
        {
            if(context.ExitEvent.WaitOne(sleep)) break;

            sw.Reset();
            sw.Start();

            DoWork();

            sw.Stop();

            var time = sw.Elapsed;
            if(time < _interval)
                sleep = (int)(_interval - time).TotalMilliseconds;
            else
                sleep = 0;
        }
        context.ExitEvent.Close();
    }

    public void Dispose()
    {
        Stop();
        GC.SuppressFinalize(this);
    }
}

答案 5 :(得分:0)

如何使用委托方法f(),将它们排队到堆栈,并在每个委托完成时弹出堆栈?当然,你还需要计时器。

答案 6 :(得分:0)

一个简单的线程是实现这一目标的最简单方法。你仍然不能确定你所谓的“准确”,但它应该是关闭的....你也可以决定是否要跳过应该发生或试图抓住的电话备份...这是创建线程的简单帮助程序。

public static Thread StartTimer(TimeSpan interval, Func<bool> operation)
{
    Thread t = new Thread(new ThreadStart(
        delegate()
        {
            DateTime when = DateTime.Now;
            TimeSpan wait = interval;
            while (true)
            {
                Thread.Sleep(wait);
                if (!operation())
                    return;
                DateTime dt = DateTime.Now;
                when += interval;
                while (when < dt)
                    when += interval;
                wait = when - dt;
            }
        }
    ));
    t.IsBackground = true;
    t.Start();
    return t;
}

答案 7 :(得分:0)

为了那些在这里寻找“重新入侵”的人们的利益:(我知道这对于原始问题可能为时已晚) 如果一个人不反对使用已经提供此类功能的开源库,我已经通过使用Quartz.NET的实现成功实现了这一点。 当您创建作业并附加触发器时,您可以指定如果先前的触发器尚未完成执行它的作业应该执行的操作