Best way to create a “run-once” time delayed funct

2020-02-08 05:10发布

I am trying to create a function that takes in an Action and a Timeout, and executes the Action after the Timeout. The function is to be non-blocking. The function must be thread safe. I also really, really want to avoid Thread.Sleep().

So far, the best I can do is this:

long currentKey = 0;
ConcurrentDictionary<long, Timer> timers = new ConcurrentDictionary<long, Timer>();

protected void Execute(Action action, int timeout_ms)
{
    long currentKey = Interlocked.Increment(ref currentKey);
    Timer t = new Timer(
      (key) =>
         {
           action();
           Timer lTimer;
           if(timers.TryRemove((long)key, out lTimer))
           {
               lTimer.Dispose();
           }
         }, currentKey, Timeout.Infinite, Timeout.Infinite
      );

     timers[currentKey] = t;
     t.Change(timeout_ms, Timeout.Infinite);
}

The problem is that calling Dispose() from the callback itself cannot be good. I am unsure if it is safe to "fall off" the end, i.e. Timers are considered live while their lambdas are executing, but even if this is the case I'd rather dispose it properly.

The "fire once with a delay" seems like such a common problem that there should be an easy way to do this, probably some other library in System.Threading I am missing, but right now the only solution I can think of is modification of the above with a dedicated cleanup task running on an interval. Any advice?

标签: c# timer
12条回答
Bombasti
2楼-- · 2020-02-08 05:20

There is nothing built-in to .Net 4 to do this nicely. Thread.Sleep or even AutoResetEvent.WaitOne(timeout) are not good - they will tie up thread pool resources, I have been burned trying this!

The lightest weight solution is to use a timer - particularly if you will have many tasks to throw at it.

First make a simple scheduled task class:

class ScheduledTask
{
    internal readonly Action Action;
    internal System.Timers.Timer Timer;
    internal EventHandler TaskComplete;

    public ScheduledTask(Action action, int timeoutMs)
    {
        Action = action;
        Timer = new System.Timers.Timer() { Interval = timeoutMs };
        Timer.Elapsed += TimerElapsed;            
    }

    private void TimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        Timer.Stop();
        Timer.Elapsed -= TimerElapsed;
        Timer = null;

        Action();
        TaskComplete(this, EventArgs.Empty);
    }
}

Then, create a scheduler class - again, very simple:

class Scheduler
{        
    private readonly ConcurrentDictionary<Action, ScheduledTask> _scheduledTasks = new ConcurrentDictionary<Action, ScheduledTask>();

    public void Execute(Action action, int timeoutMs)
    {
        var task = new ScheduledTask(action, timeoutMs);
        task.TaskComplete += RemoveTask;
        _scheduledTasks.TryAdd(action, task);
        task.Timer.Start();
    }

    private void RemoveTask(object sender, EventArgs e)
    {
        var task = (ScheduledTask) sender;
        task.TaskComplete -= RemoveTask;
        ScheduledTask deleted;
        _scheduledTasks.TryRemove(task.Action, out deleted);
    }
}

It can be called as follows - and is very lightweight:

var scheduler = new Scheduler();

scheduler.Execute(() => MessageBox.Show("hi1"), 1000);
scheduler.Execute(() => MessageBox.Show("hi2"), 2000);
scheduler.Execute(() => MessageBox.Show("hi3"), 3000);
scheduler.Execute(() => MessageBox.Show("hi4"), 4000);
查看更多
走好不送
3楼-- · 2020-02-08 05:23

treze's code is working just fine. This might help the ones who have to use older .NET versions:

private static volatile List<System.Threading.Timer> _timers = new List<System.Threading.Timer>();
private static object lockobj = new object();
public static void SetTimeout(Action action, int delayInMilliseconds)
{
    System.Threading.Timer timer = null;
    var cb = new System.Threading.TimerCallback((state) =>
    {
        lock (lockobj)
            _timers.Remove(timer);
        timer.Dispose();
        action();
    });
    lock (lockobj)
        _timers.Add(timer = new System.Threading.Timer(cb, null, delayInMilliseconds, System.Threading.Timeout.Infinite));
}
查看更多
走好不送
4楼-- · 2020-02-08 05:24

This may be a little bit late, but here is the solution I am currently using to handle delayed execution:

public class OneShotTimer
{

    private volatile readonly Action _callback;
    private OneShotTimer(Action callback, long msTime)
    {
        _callback = callback;
        var timer = new Threading.Timer(TimerProc);
        timer.Change(msTime, Threading.Timeout.Infinite);
    }

    private void TimerProc(object state)
    {
        try {
            // The state object is the Timer object. 
            ((Threading.Timer)state).Dispose();
            _callback.Invoke();
        } catch (Exception ex) {
            // Handle unhandled exceptions
        }
    }

    public static OneShotTimer Start(Action callback, TimeSpan time)
    {
        return new OneShotTimer(callback, Convert.ToInt64(time.TotalMilliseconds));
    }
    public static OneShotTimer Start(Action callback, long msTime)
    {
        return new OneShotTimer(callback, msTime);
    }
}

You can use it like this:

OneShotTimer.Start(() => DoStuff(), TimeSpan.FromSeconds(1))
查看更多
SAY GOODBYE
5楼-- · 2020-02-08 05:26

The model you have, using a one-shot timer, is definitely the way to go. You certainly don't want to create a new thread for every one of them. You could have a single thread and a priority queue of actions keyed on time, but that's needless complexity.

Calling Dispose in the callback probably isn't a good idea, although I'd be tempted to give it a try. I seem to recall doing this in the past, and it worked okay. But it's kind of a wonky thing to do, I'll admit.

You can just remove the timer from the collection and not dispose it. With no references to the object, it will be eligible for garbage collection, meaning that the Dispose method will be called by the finalizer. Just not as timely as you might like. But it shouldn't be a problem. You're just leaking a handle for a brief period. As long as you don't have thousands of these things sitting around un-disposed for a long period, it's not going to be a problem.

Another option is to have a queue of timers that remains allocated, but deactivated (i.e. their timeout and intervals set to Timeout.Infinite). When you need a timer, you pull one from the queue, set it, and add it to your collection. When the timeout expires, you clear the timer and put it back on the queue. You can grow the queue dynamically if you have to, and you could even groom it from time to time.

That'll prevent you from leaking one timer for every event. Instead, you'll have a pool of timers (much like the Thread Pool, no?).

查看更多
再贱就再见
6楼-- · 2020-02-08 05:36

I use this method to schedule a task for a specific time:

public void ScheduleExecute(Action action, DateTime ExecutionTime)
{
    Task WaitTask = Task.Delay(ExecutionTime.Subtract(DateTime.Now));
    WaitTask.ContinueWith(() => action());
    WaitTask.Start();
}

It should be noted that this only works for about 24 days out because of int32 max value.

查看更多
Anthone
7楼-- · 2020-02-08 05:36

If you don't care much about the granularity of time, you can create one timer that ticks every second and checks for expired Actions that need to be queued on the ThreadPool. Just use the stopwatch class to check for timeout.

You can use your current approach, except your Dictionary will have Stopwatch as its Key and Action as its Value. Then you just iterate on all the KeyValuePairs and find the Stopwatch that expires, queue the Action, then remove it. You'll get better performance and memory usage from a LinkedList however (since you'll be enumerating the whole thing every time and removing an item is easier).

查看更多
登录 后发表回答