Created
March 4, 2017 17:35
-
-
Save lbargaoanu/1b2aeeb40affa5242c924efc8ddd40a4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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