views:

282

answers:

7

I'm trying to invoke a method f() every t time, but if the previous invocation of f() has not finished yet, wait until it's finished.

I've read a bit about the available timers (this is a useful link) but couldn't find any good way of doing what I want, save for manually writing it all. Any help about how to achieve this will be appreciated, though I fear I might not be able to find a simple solution using timers.

To clarify, if t is one second, and f() runs the arbitrary durations I've written below, then:

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)

Notice that the nature of this timer is that f() will not need to be safe regarding re-entrance, and a thread pool of size 1 is enough here.

+3  A: 

You could just use a 'global' level var (or more likely, a public property in the same class as f()) which returns true if f() is already running.

So if f() was in a class named TimedEvent, the first thing f() would do is set Running true

That way your timer fires every second, then launches the timed event if it isnt already running
if (!timedEvent.Running) timedEvent.f()

You commented that f() wouldnt repeat immediately if it took longer than the timer interval. Thats a fair point. I would probably include logic like that inside f() so that Running stays true. So it would look something like this:

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;   
}
PaulG
If you do this, beware of race conditions.
Jason Williams
True, but questioner did state that it doesn't need to be multi-thread safe
PaulG
Very elegant, though it means a 2nd `f` will not immediately follow a 10.5 second `f`, doesn't it? It will wait 0.5 seconds between them...
Oak
@Oak, good point. I added an edit.
PaulG
+1  A: 

You can use a non-restarting timer, then manually restart the timer after the method finishes.

Note that this will result in timing that is somewhat different from what you're asking for. (There will always be a gap of t time between invocations)

You could solve that by setting the interval to lastTick + t - Now, and running the method immediately if that's <= 0.

Beware of race conditions if you need to stop the timer.

SLaks
A: 

You cannot get a timer to call you at exactly scheduled intervals. All timers do is call you back no sooner than the requested time.

Some timers are better than others (e.g. Windows.Forms.Timer is very erratic and unreliable compared to System.Threading.Timer)

To stop your timer being called re-entrantly, one approach is to Stop the timer while your method is running. (Depending on the type of timer you use, you either stop it and start it again when your handler exits, or with some timers you can request a single callback rather than repeating callbacks, so each execution of your handler simply enqueues the next call).

To keep the timing relatively even between these calls you can record the time since your handler last executed and use that to calculate the delay until the next event is required. e.g. If you want to be called once per second and your timer completed provcessing at 1.02s, then you can set up the next timer callback at a duration of 0.98s to accomodate the fact that you've already "used up" part of the next second during your processing.

Jason Williams
+2  A: 

Use a System.Threading.Timer. Initialize it with a period of 0 so it acts like a one-shot timer. When f() completes, call its Change() method to recharge it again.

Hans Passant
A: 

A straightforward solution:

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);
    }
}
max
A: 

How about using delegates to method f(), queuing them to a stack, and popping the stack as each delegate completes? You still need the timer, of course.

code4life
A: 

A simple thread is the easiest way to achieve this. Your still not going to be certain that your called 'precisely' when you want, but it should be close.... Also you can decide if you want to skip calls that should happen or attempt to catch back up... Here is simple helper routine for creating the thread.

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;
}
csharptest.net