Non-reentrant timers

2019-01-26 11:38发布

I have a function that I want to invoke every x seconds, but I want it to be thread-safe.

Can I set up this behavior when I am creating the timer? (I don't mind which .NET timer I use, I just want it to be thread-safe).

I know I can implement locks inside my callback function, but I think it would be more elegant if it were in the timer level.

My callback function, and environment are not related to a UI.

[Edit 1] I just don't want there to be more than one thread inside my callback function.

[Edit 2] I want to keep the locking inside the timer level, because the timer is responsible for when to call my callback, and here there is a particular situation when I don't want to call my callback function. So I think when to call is the responsibility of the timer.

5条回答
萌系小妹纸
2楼-- · 2019-01-26 12:18

Complementing Tim Lloyd's solution for System.Timers.Timer, here's a solution to prevent reentrancy for cases where you want to use System.Threading.Timer instead.

TimeSpan DISABLED_TIME_SPAN = TimeSpan.FromMilliseconds(-1);

TimeSpan interval = TimeSpan.FromSeconds(1);
Timer timer = null; // assign null so we can access it inside the lambda

timer = new Timer(callback: state =>
{
  doSomeWork();
  try
  {
    timer.Change(interval, DISABLED_TIME_SPAN);
  }
  catch (ObjectDisposedException timerHasBeenDisposed)
  {
  }
}, state: null, dueTime: interval, period: DISABLED_TIME_SPAN);

I believe you don't want interval to be accessed inside of the callback, but that is be easy to fix, if you want to: Put the above into a NonReentrantTimer class that wraps the BCL's Timer class. You would then pass the doSomeWork callback in as a parameter. An example of such a class:

public class NonReentrantTimer : IDisposable
{
    private readonly TimerCallback _callback;
    private readonly TimeSpan _period;
    private readonly Timer _timer;

    public NonReentrantTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period)
    {
        _callback = callback;
        _period = period;
        _timer = new Timer(Callback, state, dueTime, DISABLED_TIME_SPAN);
    }

    private void Callback(object state)
    {
        _callback(state);
        try
        {
            _timer.Change(_period, DISABLED_TIME_SPAN);
        }
        catch (ObjectDisposedException timerHasBeenDisposed)
        {
        }
    }


    public void Dispose()
    {
        _timer.Dispose();
    }
}
查看更多
戒情不戒烟
3楼-- · 2019-01-26 12:22

How timer could know about your shared data?

Timer callback is executed on some ThreadPool thread. So you will have at least 2 threads:

  1. Your main thread where timer is created and launched;
  2. Thread from ThreadPool for launching callback.

And it is your responsibility to provide correct work with your shared data.

Re edits: chibacity provided the perfect example.

查看更多
Lonely孤独者°
4楼-- · 2019-01-26 12:26

I'm guessing, as your question is not entirely clear, that you want to ensure that your timer cannot re-enter your callback whilst you are processing a callback, and you want to do this without locking. You can achieve this using a System.Timers.Timer and ensuring that the AutoReset property is set to false. This will ensure that you have to trigger the timer on each interval manually, thus preventing any reentrancy:

public class NoLockTimer : IDisposable
{
    private readonly Timer _timer;

    public NoLockTimer()
    {
        _timer = new Timer { AutoReset = false, Interval = 1000 };

        _timer.Elapsed += delegate
        {
            //Do some stuff

            _timer.Start(); // <- Manual restart.
        };

        _timer.Start();
    }

    public void Dispose()
    {
        if (_timer != null)
        {
            _timer.Dispose();
        }
    }
} 
查看更多
倾城 Initia
5楼-- · 2019-01-26 12:32

I know I can implement locks inside my callback function, but I think it will be more elegant if it will be in the timer level

If locking is necessary then how could a timer arrange that? You're looking for a magical freebie.

Re Edit1:

Your choices are System.Timers.Timer and System.Threading.Timer, both need precautions against re-entrance. See this page and look for the Dealing with Timer Event Reentrance section.

查看更多
做个烂人
6楼-- · 2019-01-26 12:32
/// <summary>
/// The C#6 version
/// </summary>
public class TimerNicer : IDisposable {
    private Action OnElapsed { get; }

    [NotNull]
    private System.Timers.Timer Timer { get; } = new System.Timers.Timer { AutoReset = false, Interval = 1 };

    public TimerNicer(Action onElapsed) {

        this.OnElapsed = onElapsed ?? ( () => {
        } );

        this.Timer.Elapsed += (sender, args) => {

            this.Timer.Stop();    // Why not stop the timer here with this?

            try {
                this.OnElapsed(); // do stuff here
            }
            catch ( Exception exception ) {
                Console.WriteLine( exception );
            }
            finally {
                this.Timer.Start();
            }
        };

        this.Timer.Start();
    }

    public void Dispose() => this.Timer.Dispose();
}
查看更多
登录 后发表回答