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.
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();
}
}
}
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();
}
}
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.
using System;
using System.Diagnostics;
/// <summary>
/// Updated the code.
/// </summary>
public class NicerFormTimer : IDisposable {
public void Dispose() {
using ( this.Timer ) { }
GC.SuppressFinalize( this );
}
private System.Windows.Forms.Timer Timer { get; }
/// <summary>
/// Perform an <paramref name="action" /> after the given interval (in <paramref name="milliseconds" />).
/// </summary>
/// <param name="action"></param>
/// <param name="repeat">Perform the <paramref name="action" /> again. (Restarts the <see cref="Timer" />.)</param>
/// <param name="milliseconds"></param>
public NicerFormTimer( Action action, Boolean repeat, Int32? milliseconds = null ) {
if ( action == null ) {
return;
}
this.Timer = new System.Windows.Forms.Timer {
Interval = milliseconds.GetValueOrDefault( 1000 )
};
this.Timer.Tick += ( sender, args ) => {
try {
this.Timer.Stop();
action();
}
catch ( Exception exception ) {
Debug.WriteLine( exception );
}
finally {
if ( repeat ) {
this.Timer.Start();
}
}
};
this.Timer.Start();
}
}
/// <summary>
/// Updated the code.
/// </summary>
public class NicerSystemTimer : IDisposable {
public void Dispose() {
using ( this.Timer ) { }
GC.SuppressFinalize( this );
}
private System.Timers.Timer Timer { get; }
/// <summary>
/// Perform an <paramref name="action" /> after the given interval (in <paramref name="milliseconds" />).
/// </summary>
/// <param name="action"></param>
/// <param name="repeat">Perform the <paramref name="action" /> again. (Restarts the <see cref="Timer" />.)</param>
/// <param name="milliseconds"></param>
public NicerSystemTimer( Action action, Boolean repeat, Double? milliseconds = null ) {
if ( action == null ) {
return;
}
this.Timer = new System.Timers.Timer {
AutoReset = false,
Interval = milliseconds.GetValueOrDefault( 1000 )
};
this.Timer.Elapsed += ( sender, args ) => {
try {
this.Timer.Stop();
action();
}
catch ( Exception exception ) {
Debug.WriteLine( exception );
}
finally {
if ( repeat ) {
this.Timer.Start();
}
}
};
this.Timer.Start();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With