Skip to content

Instantly share code, notes, and snippets.

@lbargaoanu
Created March 4, 2017 17:35
Show Gist options
  • Save lbargaoanu/1b2aeeb40affa5242c924efc8ddd40a4 to your computer and use it in GitHub Desktop.
Save lbargaoanu/1b2aeeb40affa5242c924efc8ddd40a4 to your computer and use it in GitHub Desktop.
namespace Scheduler
{
using System;
using System.Threading;
using System.Diagnostics;
using System.Collections.Generic;
public static class JobScheduler
{
static readonly LinkedList<Job> jobs = new LinkedList<Job>();
static readonly Timer timer = new Timer(TimerFunction);
static readonly SynchronizationContext defaultContext = new SynchronizationContext();
/// <summary>
/// This is the default ThreadPool SynchronizationContext.
/// </summary>
public static SynchronizationContext DefaultContext
{
get
{
return defaultContext;
}
}
static int interval;
class Job
{
readonly object state;
readonly int interval;
readonly SynchronizationContext context;
readonly SendOrPostCallback callback;
DateTime nextRunDate;
public Job(int millisecondsInterval, SendOrPostCallback action, object state, SynchronizationContext context)
{
this.state = state;
this.nextRunDate = TruncatedUtcNow();
this.context = context == null ? DefaultContext : context;
callback = action;
interval = millisecondsInterval;
}
public SendOrPostCallback Callback
{
get
{
return callback;
}
}
public int Interval
{
get
{
return interval;
}
}
public void CheckExecute(ref DateTime utcNow)
{
Debug.WriteLine("[JobScheduler] Now: " + utcNow.ToString("mm:ss.fff") + " nextRunDate: " + nextRunDate.ToString("mm:ss.fff") + " interval: " + interval);
if(nextRunDate > utcNow)
{
return;
}
nextRunDate = utcNow.AddMilliseconds(interval);
context.Post(callback, state);
}
}
/// <summary>
/// Register a delegate to be called with some frequency.
/// The state parameter is null and the SynchronizationContext is the same as the caller.
/// </summary>
/// <param name="millisecondsInterval">the frequency of the calls</param>
/// <param name="action">the callback to be called</param>
public static void Register(int millisecondsInterval, SendOrPostCallback action)
{
Register(millisecondsInterval, action, null);
}
/// <summary>
/// Register a delegate to be called with some frequency.
/// The SynchronizationContext is the same as the caller.
/// </summary>
/// <param name="millisecondsInterval">the frequency of the calls</param>
/// <param name="action">the callback to be called</param>
/// <param name="state">an optional state parameter to be passed to the callback</param>
public static void Register(int millisecondsInterval, SendOrPostCallback action, object state)
{
Register(millisecondsInterval, action, state, SynchronizationContext.Current);
}
/// <summary>
/// Register a delegate to be called on a given SynchronizationContext with some frequency.
/// </summary>
/// <param name="millisecondsInterval">the frequency of the calls</param>
/// <param name="action">the callback to be called</param>
/// <param name="state">an optional state parameter to be passed to the callback</param>
/// <param name="context">the SynchronizationContext to use for asynchronously executing the callback</param>
public static void Register(int millisecondsInterval, SendOrPostCallback action, object state, SynchronizationContext context)
{
if(millisecondsInterval <= 0)
{
throw new ArgumentOutOfRangeException("millisecondsInterval");
}
if(action == null)
{
throw new ArgumentNullException("action");
}
Job job = new Job(millisecondsInterval, action, state, context);
Trace.WriteLine("[JobScheduler] BEGIN Register");
lock(jobs)
{
jobs.AddLast(job);
interval = Gcd(millisecondsInterval, interval);
timer.Change(0, interval);
}
Trace.WriteLine("[JobScheduler] END Register");
}
/// <summary>
/// Unregister the first occurrance of the specified delegate in the list of registered jobs.
/// </summary>
/// <param name="action">the delegate to unregister</param>
public static void Unregister(SendOrPostCallback action)
{
LinkedListNode<Job> node;
LinkedListNode<Job> foundNode = null;
Trace.WriteLine("[JobScheduler] BEGIN Unregister");
lock(jobs)
{
// look for the node that has the given callback
node = jobs.First;
while(node != null)
{
if(node.Value.Callback == action)
{
foundNode = node;
break;
}
node = node.Next;
}
if(foundNode == null)
{
// not found
return;
}
// remove the node before recalculating the new gcd
jobs.Remove(foundNode);
// passing the removed frequency might cause the new Gcd to be found faster if there is another job with the same frequency
RecalculateInterval(foundNode.Value.Interval);
// we must call Change even if the interval didn't change because we might have skipped a tick in TimerFunction while under the lock
timer.Change(0, interval);
}
Trace.WriteLine("[JobScheduler] END Unregister");
}
private static void RecalculateInterval(int removedFrequency)
{
// calculate the new gcd of the frequencies and save the old one, just in case we detect it can't change
int oldInterval = interval;
interval = 0;
foreach(Job job in jobs)
{
// there is no use to go on if there is another job with the same frequency, the gcd will not change
if(job.Interval == removedFrequency)
{
interval = oldInterval;
return;
}
interval = Gcd(interval, job.Interval);
}
}
static void TimerFunction(object unused)
{
if(!Monitor.TryEnter(jobs))
{
Trace.WriteLine("[JobScheduler] Jobs taking too long to execute or a Register/Unregister is in progress. Interval: " + interval + ". Job count: " + jobs.Count);
return;
}
try
{
DateTime utcNow = TruncatedUtcNow();
foreach(Job job in jobs)
{
job.CheckExecute(ref utcNow);
}
}
finally
{
Monitor.Exit(jobs);
}
}
static DateTime TruncatedUtcNow()
{
DateTime utcNow = DateTime.UtcNow;
return new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, utcNow.Hour, utcNow.Minute, utcNow.Second, utcNow.Millisecond);
}
static int Gcd(int a, int b)
{
while(b != 0)
{
int oldA = a;
a = b;
b = oldA % b;
}
return a;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment