#region CPL License /* Nuclex Framework Copyright (C) 2002-2009 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion using System; using System.Collections.Generic; using System.Threading; using System.Diagnostics; using Nuclex.Support.Collections; namespace Nuclex.Support.Scheduling { /// Schedules actions for execution at a future point in time public class Scheduler : IDisposable { /// One tick is 100 ns, meaning 10000 ticks equal 1 ms private const long TicksPerMillisecond = 10000; #region class TimeSourceSingleton /// /// Manages the singleton instance of the scheduler's default time source /// private class TimeSourceSingleton { /// /// Explicit static constructor to guarantee the singleton is initialized only /// when a static member of this class is accessed. /// static TimeSourceSingleton() { } // Do not remove! /// The singleton instance of the default time source internal static readonly ITimeSource Instance = Scheduler.CreateDefaultTimeSource(); } #endregion // class TimeSourceSingleton #region class Notification /// Scheduled notification private class Notification { /// Initializes a new notification /// /// Interval in which the notification will re-executed /// /// /// Time source ticks the notification is next due at /// /// /// Absolute time in UTC at which the notification is due /// /// /// Callback to be invoked when the notification is due /// public Notification( long intervalTicks, long nextDueTicks, DateTime absoluteUtcTime, WaitCallback callback ) { this.IntervalTicks = intervalTicks; this.NextDueTicks = nextDueTicks; this.AbsoluteUtcTime = absoluteUtcTime; this.Callback = callback; this.Cancelled = false; } /// /// Ticks specifying the interval in which the notification will be re-executed /// public long IntervalTicks; /// Next due time for this notification public long NextDueTicks; /// Absolute time in UTC at which the notification is due /// /// Only stored for notifications scheduled in absolute time, meaning they /// have to be adjusted if the system date/time changes /// public DateTime AbsoluteUtcTime; /// Callback that will be invoked when the notification is due public WaitCallback Callback; /// Whether the notification has been cancelled public bool Cancelled; } #endregion // class Notification #region class NotificationComparer /// Compares two notifications to each other private class NotificationComparer : IComparer { /// The default instance of the notification comparer public static readonly NotificationComparer Default = new NotificationComparer(); /// Compares two notifications to each other based on their time /// Notification that will be compared on the left side /// Notification that will be comapred on the right side /// The relation of the two notification's times to each other public int Compare(Notification left, Notification right) { if(left.NextDueTicks > right.NextDueTicks) { return -1; } else if(left.NextDueTicks < right.NextDueTicks) { return 1; } else { return 0; } } } #endregion // class NotificationComparer /// Initializes a new scheduler using the default time source public Scheduler() : this(DefaultTimeSource) { } /// Initializes a new scheduler using the specified time source /// Source source the scheduler will use public Scheduler(ITimeSource timeSource) { this.dateTimeAdjustedDelegate = new EventHandler(dateTimeAdjusted); this.timeSource = timeSource; this.timeSource.DateTimeAdjusted += this.dateTimeAdjustedDelegate; this.notifications = new PriorityQueue(NotificationComparer.Default); this.notificationWaitEvent = new AutoResetEvent(false); this.timerThread = new Thread(new ThreadStart(runTimerThread)); this.timerThread.Name = "Nuclex.Support.Scheduling.Scheduler"; this.timerThread.Priority = ThreadPriority.Highest; this.timerThread.IsBackground = true; this.timerThread.Start(); } /// Immediately releases all resources owned by the instance public void Dispose() { if(this.timerThread != null) { this.endRequested = true; this.notificationWaitEvent.Set(); // Wait for the timer thread to exit. If it doesn't exit in 10 seconds (which is // a lot of time given that it doesn't do any real work), forcefully abort // the thread. This may risk some leaks, but it's the only thing we can do. Trace.Assert( this.timerThread.Join(2500), "Scheduler timer thread did not exit in time" ); // Unsubscribe from the time source to avoid surprise events during or // after shutdown if(this.timeSource != null) { this.timeSource.DateTimeAdjusted -= this.dateTimeAdjustedDelegate; this.timeSource = null; } // Get rid of the notification wait event now that we've made sure that // the timer thread is down. this.notificationWaitEvent.Close(); // Help the GC a bit this.notificationWaitEvent = null; this.notifications = null; // Set to null so we don't attempt to end the thread again if Dispose() is // called multiple times. this.timerThread = null; } } /// Schedules a notification at the specified absolute time /// /// Absolute time at which the notification will occur /// /// /// Callback that will be invoked when the notification is due /// /// A handle that can be used to cancel the notification /// /// The notification is scheduled for the indicated absolute time. If the system /// enters/leaves daylight saving time or the date/time is changed (for example /// when the system synchronizes with an NTP server), this will affect /// the notification. So if you need to be notified after a fixed time, use /// the NotifyIn() method instead. /// public object NotifyAt(DateTime notificationTime, WaitCallback callback) { if(notificationTime.Kind == DateTimeKind.Unspecified) { throw new ArgumentException( "Notification time is neither UTC or local", "notificationTime" ); } DateTime notificationTimeUtc = notificationTime.ToUniversalTime(); DateTime now = this.timeSource.CurrentUtcTime; long remainingTicks = notificationTimeUtc.Ticks - now.Ticks; long nextDueTicks = this.timeSource.Ticks + remainingTicks; return scheduleNotification( new Notification( 0, nextDueTicks, notificationTimeUtc, callback ) ); } /// Schedules a notification after the specified time span /// Delay after which the notification will occur /// /// Callback that will be invoked when the notification is due /// /// A handle that can be used to cancel the notification public object NotifyIn(TimeSpan delay, WaitCallback callback) { return scheduleNotification( new Notification( 0, this.timeSource.Ticks + delay.Ticks, DateTime.MinValue, callback ) ); } /// /// Schedules a notification after the specified amount of milliseconds /// /// /// Number of milliseconds after which the notification will occur /// /// /// Callback that will be invoked when the notification is due /// /// A handle that can be used to cancel the notification public object NotifyIn(int delayMilliseconds, WaitCallback callback) { return scheduleNotification( new Notification( 0, this.timeSource.Ticks + ((long)delayMilliseconds * TicksPerMillisecond), DateTime.MinValue, callback ) ); } /// /// Schedules a recurring notification after the specified time span /// /// Delay after which the first notification will occur /// Interval at which the notification will be repeated /// /// Callback that will be invoked when the notification is due /// /// A handle that can be used to cancel the notification public object NotifyEach(TimeSpan delay, TimeSpan interval, WaitCallback callback) { return scheduleNotification( new Notification( interval.Ticks, this.timeSource.Ticks + delay.Ticks, DateTime.MinValue, callback ) ); } /// /// Schedules a recurring notification after the specified amount of milliseconds /// /// /// Milliseconds after which the first notification will occur /// /// /// Interval in milliseconds at which the notification will be repeated /// /// /// Callback that will be invoked when the notification is due /// /// A handle that can be used to cancel the notification public object NotifyEach( int delayMilliseconds, int intervalMilliseconds, WaitCallback callback ) { return scheduleNotification( new Notification( (long)intervalMilliseconds * TicksPerMillisecond, this.timeSource.Ticks + ((long)delayMilliseconds * TicksPerMillisecond), DateTime.MinValue, callback ) ); } /// Cancels a scheduled notification /// /// Handle of the notification that will be cancelled /// public void Cancel(object notificationHandle) { Notification notification = notificationHandle as Notification; if(notification != null) { notification.Cancelled = true; } } /// Returns the default time source for the scheduler public static ITimeSource DefaultTimeSource { get { return TimeSourceSingleton.Instance; } } /// Creates a new default time source for the scheduler /// /// Whether the specialized windows time source should be used /// /// The newly created time source internal static ITimeSource CreateTimeSource(bool useWindowsTimeSource) { if(useWindowsTimeSource) { return new WindowsTimeSource(); } else { return new GenericTimeSource(); } } /// Creates a new default time source for the scheduler /// The newly created time source internal static ITimeSource CreateDefaultTimeSource() { return CreateTimeSource(WindowsTimeSource.Available); } /// Called when the system date/time have been adjusted /// Time source which detected the adjustment /// Not used private void dateTimeAdjusted(object sender, EventArgs arguments) { lock(this.timerThread) { long currentTicks = this.timeSource.Ticks; DateTime currentTime = this.timeSource.CurrentUtcTime; PriorityQueue updatedQueue = new PriorityQueue( NotificationComparer.Default ); // Copy all notifications from the original queue to a new one, adjusting // those with an absolute notification time along the way to a new due tick while(this.notifications.Count > 0) { Notification notification = this.notifications.Dequeue(); if(!notification.Cancelled) { // If this notification has an absolute due time, adjust its due tick if(notification.AbsoluteUtcTime != DateTime.MinValue) { // Combining recurrent notifications with absolute time isn't allowed Debug.Assert(notification.IntervalTicks == 0); // Make the adjustment long remainingTicks = (notification.AbsoluteUtcTime - currentTime).Ticks; notification.NextDueTicks = currentTicks + remainingTicks; } // Notification processed, move it over to the next priority queue updatedQueue.Enqueue(notification); } } // Replace the working queue with the update queue this.notifications = updatedQueue; } this.notificationWaitEvent.Set(); } /// Schedules a notification for processing by the timer thread /// Notification that will be scheduled /// The scheduled notification private object scheduleNotification(Notification notification) { lock(this.timerThread) { this.notifications.Enqueue(notification); // If this notification has become that next due notification, wake up // the timer thread so it can adjust its sleep period. if(ReferenceEquals(this.notifications.Peek(), notification)) { this.notificationWaitEvent.Set(); } } return notification; } /// Executes the timer thread private void runTimerThread() { Notification nextDueNotification; lock(this.timerThread) { nextDueNotification = getNextDueNotification(); } // Keep processing notifications until we're told to quit for(; ; ) { // Wait until the nextmost notification is due or something else wakes us up if(nextDueNotification == null) { this.notificationWaitEvent.WaitOne(); } else { long remainingTicks = nextDueNotification.NextDueTicks - this.timeSource.Ticks; this.timeSource.WaitOne(this.notificationWaitEvent, remainingTicks); } // Have we been woken up because the Scheduler is being disposed? if(this.endRequested) { break; } // Process all notifications that are due by handing them over to the thread pool. // The notification queue might have been updated while we were sleeping, so // look for the notification that is due next again long ticks = this.timeSource.Ticks; lock(this.timerThread) { for(; ; ) { nextDueNotification = getNextDueNotification(); if(nextDueNotification == null) { break; } // If the next notification is more than a millisecond away, we've reached // the end of the notifications we need to process. long remainingTicks = (nextDueNotification.NextDueTicks - ticks); if(remainingTicks >= TicksPerMillisecond) { break; } if(!nextDueNotification.Cancelled) { ThreadPool.QueueUserWorkItem(nextDueNotification.Callback); } this.notifications.Dequeue(); if(nextDueNotification.IntervalTicks != 0) { nextDueNotification.NextDueTicks += nextDueNotification.IntervalTicks; this.notifications.Enqueue(nextDueNotification); } } // for } // lock } // for } /// Retrieves the notification that is due next /// The notification that is due next private Notification getNextDueNotification() { if(this.notifications.Count == 0) { return null; } else { Notification nextDueNotification = this.notifications.Peek(); while(nextDueNotification.Cancelled) { this.notifications.Dequeue(); nextDueNotification = this.notifications.Peek(); } return nextDueNotification; } } /// Time source used by the scheduler private ITimeSource timeSource; /// Thread that will wait for the next scheduled event private Thread timerThread; /// Notifications in the scheduler's queue private PriorityQueue notifications; /// Event used by the timer thread to wait for the next notification private AutoResetEvent notificationWaitEvent; /// Whether the timer thread should end private volatile bool endRequested; /// Delegate for the dateTimeAdjusted() method private EventHandler dateTimeAdjustedDelegate; } } // namespace Nuclex.Support.Scheduling