//------------------------------------------------------------------------------ // // Microsoft Research Singularity // // Copyright (c) Microsoft Corporation. All rights reserved. // // Description: MinScheduler.cs // // Minimal round-robin style without priorities scheduler. // // MinScheduler favors thread that have recently become unblocked // and tries to avoid reading the clock or reseting the timer as // much as possible. // // The minimal scheduler maintains two queues of threads that can // be scheduled. The unblockedThreads queue contains threads which // have become unblocked during this scheduling quantum; mostly, // these are threads that were unblocked by the running thread. // The runnableThreads queue contains all other threads that are // currently runnable. If the current thread blocks, MinScheduler // will schedule threads from the unblockedThread queue, without // reseting the timer. When the timer finally fires, MinScheduler // moves all unblockedThreads to the end of the runnableThreads // queue and schedules the next runnableThread. //------------------------------------------------------------------------------ using System; using System.Collections; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using Microsoft.Bartok.Options; using Microsoft.Singularity; using Microsoft.Singularity.Io; using Microsoft.Singularity.Scheduling; namespace Microsoft.Singularity.Scheduling { /// /// Summary description for MinScheduler. /// [NoCCtor] [CLSCompliant(false)] public class MinScheduler : Scheduler { /// /// /// Constructor for MinScheduler object /// /// public MinScheduler() { // If busy, don't run for more than 10ms on same task. minSlice = TimeSpan.FromMilliseconds(10); // Note that we have made the idleSlice small always. // This is necessary in the MP case - otherwise a // CPU seeing no work will go to sleep for a month and // there is no mechanism to be woken when another CPU // has excess work. (This support should be added // eventually if we intend to use this scheduler.) idleSlice = TimeSpan.FromMilliseconds(100); affinity = 0; // Create a scheduler lock this.runnableLock = new SchedulerLock(); // Initialize timer's spinlock this.timerLock = new SpinLock(SpinLock.Types.Timer); } /// /// Initialize min scheduler /// /// public override void Initialize() { } /// /// Finalize scheduler object /// /// public override void Finalize() { } /// /// /// Notify scheduler about new dispatcher /// /// public override void OnDispatcherInitialize(int dispatcherId) { // Min scheduler doesn't care about multiple dispatchers } /// /// Attach thread to scheduler: thread specific initializtion /// /// /// Thread to attach /// Have we called thread constructor /// public override void OnThreadStateInitialize(Thread thread, bool constructorCalled) { // Only initialize thread-local state! No locks held and interrupts on. } /// /// /// Retrieve scheduler lock - used by dispatcher to protect scheduler state /// /// /// Affinity of dispatcher making actual call /// [CLSCompliant(false)] [NoHeapAllocation] internal override SchedulerLock RetrieveSchedulerLock(int affinity) { // Return our default scheduler return MyLock; } /// /// /// Run scheduling policy. This method is called by dispatcher when both interrupts /// disabled and disptach lock is acquired. As long as multiple dispatchers don't have access /// to the this method no protection is required. /// /// /// Set the returned running thread to this affinity. /// the thread currently running /// thread state to change to for the current thread /// Current system time /// [NoHeapAllocation] public override Thread RunPolicy( int affinity, Thread currentThread, ThreadState schedulerAction, SchedulerTime currentTime) { Thread threadToReturn = null; ThreadState newState = ThreadState.Undefined; ThreadEntry entry = null; // Put current thread on a runnable queue only if it is currently in a running state if (currentThread != null) { // At this point current threads state has to be // running - when running derived scheduler state we will deduce new state // but until then it has to be running VTable.Assert(currentThread.ThreadState == ThreadState.Running); // If scheduler action is running make it runnable to have proper state transition // Currently we disallow to go from running to running if (schedulerAction == ThreadState.Running) { schedulerAction = ThreadState.Runnable; } // Change current's thread new scheudler state newState = currentThread.ChangeSchedulerState(schedulerAction); // If new state is runnable add it to runnable queue if (newState == ThreadState.Runnable) { // REVIEW: Should we make sure the thread is enqueue already? // Indicate that thread has been marked as runnable this.runnableThreads.EnqueueTail(currentThread.schedulerEntry); } } // Derived state of entry on the runnable queue can be either suspended or runnable. // Consequently first we remove an entry from the queue. If it is runnable, // we will be able to convert it to running, If it is suspended, // we will convert its real state to suspended. The first thread that marks the entry // unsuspended will be responsible for putting it on back on a runnable queue. // Check the unblocked queue first. while ((entry = this.unblockedThreads.DequeueHead()) != null) { // At this point thread direct state can be only runnable... VTable.Assert(entry.Thread.IsRunnable); // Attempt to make thread running newState = entry.Thread.ChangeSchedulerState(ThreadState.Running); // Get of the loop if we marked one of the entries as running if (newState == ThreadState.Running) { break; } // If the thread is suspended, then look for another. VTable.Assert(newState == ThreadState.Suspended); } // If no recently unblocked threads, then check the runnable queue. if (entry == null) { while ((entry = this.runnableThreads.DequeueHead()) != null) { // At this point thread direct state can be only runnable... VTable.Assert(entry.Thread.IsRunnable); // Attempt to make thread running newState = entry.Thread.ChangeSchedulerState(ThreadState.Running); // Get of the loop if we marked one of the entries as running if (newState == ThreadState.Running) { break; } // If the thread is suspended, then look for another. VTable.Assert(newState == ThreadState.Suspended); } } // We got an entry from the runnable queue that we can actually run. if (entry != null) { // Thread must realy by in the running state. VTable.Assert(newState == ThreadState.Running); // Initialize thread we about to return. threadToReturn = entry.Thread; threadToReturn.Affinity = affinity; } return threadToReturn; } /// /// /// Add thread to runnable queue. This method is called by dispatcher at the /// point when both dispatcher lock and interrupts are disabled /// /// /// Thread to add to runnable queue /// [Inline] [NoHeapAllocation] public override void AddRunnableThread(Thread thread) { ThreadState newState; // Change thread's state to runnable: Runpolicy will decide latter on // if it can actually run the thread newState = thread.ChangeSchedulerState(ThreadState.Runnable); // Add thread to runnable queue only if new state is runnable if (newState == ThreadState.Runnable) { // While we adding a thread to runnable queue someone could have called freeze on // it again - RunPolicy will notice it and remove thread from runnable queue appropriatly this.unblockedThreads.EnqueueTail(thread.schedulerEntry); } } /// /// /// Start thread - put a thread on dispatcher's runnable queue so it can be run /// /// /// Thread to start /// [NoHeapAllocation] public override void OnThreadStart(Thread thread) { // Initialize thread's affinity thread.Affinity = (int)ProcessorDispatcher.Affinity.All; // Enqueue thread into dispatcher. ProcessorDispatcher.AddRunnableThread(thread); } /// /// /// Block thread -process thread blocking including putting it on a timer queue if /// timeout is specified. /// /// /// Thread that blocks /// Amount of time for the thread to be blocked /// [NoHeapAllocation] public override void OnThreadBlocked(Thread thread, SchedulerTime stop) { // Assert preconditions VTable.Assert(thread == Thread.CurrentThread); if (stop != SchedulerTime.MaxValue) { // Enqueue timer so that if it expires we will notice it right a way EnqueueTimer(thread, stop); } // Thread is blocked now - indicate this to dispatcher. Dispatcher will make // make sure that thread's scheduler is aware of the blockage ProcessorDispatcher.SwitchContext(ThreadState.Blocked); // We just got unblocked and happilly running. We need to remove ourselves // from the timer queue if we are unblocked by signal rather than by timeout if (thread.UnblockedBy != WaitHandle.WaitTimeout) { // One of our buddies unblocked us - remove the time out. Before we can // actually assert that we were indeed unblocked by someone. VTable.Assert(thread.UnblockedBy != WaitHandle.UninitWait); // Remove a timer from the timer queue and happilly continue! RemoveTimer(thread); } } /// /// /// Unblock thread - resume thread by putting it on the dispatcher runnable queue. /// This method can be invoked by threads running on other processors /// /// /// Thread to resume /// [NoHeapAllocation] public override void OnThreadUnblocked(Thread thread) { // Only call this method if thread is Indeed blocked VTable.Assert(thread.IsBlocked); // Thread is ready to run: Add it to dispatcher ProcessorDispatcher.AddRunnableThread(thread); } /// /// /// Yield thread - suspend thread based on time slice /// /// /// Thread that is yielding /// [NoHeapAllocation] public override void OnThreadYield(Thread thread) { // Perform a context switch: If thread is runnable it will be put on a runnable queue // by dispatcher. ProcessorDispatcher.SwitchContext(ThreadState.Running); } /// /// /// Stop thread execution /// /// /// Thread that is stopping /// [NoHeapAllocation] public override void OnThreadStop(Thread thread) { // Perform a context switch: If thread is stopped it will be cleaned up, otherwise // it is suspended and will be cleaned up latter ProcessorDispatcher.SwitchContext(ThreadState.Stopped); } /// /// /// Increment frozen counter /// /// /// Thread for which to increment freeze counter /// [NoHeapAllocation] public override void OnThreadFreezeIncrement(Thread thread) { // Update threads: Freeze count thread.IncrementFreezeCounter(); } /// /// /// Decrement frozen counter /// /// /// Thread for which to update freeze counter /// [NoHeapAllocation] public override void OnThreadFreezeDecrement(Thread thread) { bool shouldPutThreadOnRunnableQueue = false; // Assert preconditions DebugStub.Assert(thread.FreezeCount > 0); // Update threads: Freeze count if (thread.DecrementFreezeCounter(ref shouldPutThreadOnRunnableQueue) == 0 && shouldPutThreadOnRunnableQueue) { // Thread became runnable - add it to runnable queue ProcessorDispatcher.AddRunnableThread(thread); } } /// /// /// Suspend thread and wait until it is suspended. /// /// /// Thread to suspend /// [NoHeapAllocation] public override void Suspend(Thread thread) { // Assert preconditions: We can't call suspend on ourselves VTable.Assert(thread != Thread.CurrentThread); // Increment freeze counter and then spin wait until thread will become suspended OnThreadFreezeIncrement(thread); // Wait until thread is capable of running while (thread.IsRunning || thread.IsRunnable) { Thread.SpinWait(100); } } /// /// /// Resume thread from being suspended /// /// /// Thread to resume /// [NoHeapAllocation] public override void Resume(Thread thread) { // Increment freeze counter and then spin wait until thread will becom suspended OnThreadFreezeDecrement(thread); } /// /// /// Dispatch timer interrupt. This method is called by dispather when interrupts are /// disabled. /// /// /// Processor affinity /// Current time /// [CLSCompliant(false)] [NoHeapAllocation] [HalLock] public override TimeSpan OnTimerInterrupt(int affinity, SchedulerTime now) { ThreadEntry entry; TimeSpan delta; // Assert pre conditions DebugStub.Assert(this.minSlice.Ticks != 0); DebugStub.Assert(this.idleSlice.Ticks != 0); // For now interrupts should be disabled when this method is called VTable.Assert(Processor.InterruptsDisabled()); // Move all of the unblockedThreads to the runnable queue. while ((entry = this.unblockedThreads.DequeueHead()) != null) { this.runnableThreads.EnqueueTail(entry); } // Acquire timer lock timerLock.Acquire(); // Unblock any threads whose timers have elapsed. while ((entry = this.timerThreads.Head) != null && entry.Thread.BlockedUntil <= now) { // Remove thread from the timer queue: There shouldn't be race with RemoveTimer // call since we are protected by a timer queue's lock this.timerThreads.Remove(entry); // Indicate to the thread that its wait completed with timeout if (entry.Thread.Unblock(WaitHandle.WaitTimeout) == WaitHandle.WaitTimeout) { // Only if we have unblocked thread and it was already in blocked state // put it on a dispatcher deferred wake up queue: otherwise scheduler will notice // expiration itself and put the thread on a runnable queue. For more info // see RunPolicy if (entry.Thread.ShouldCallSchedulerUnBlock(WaitHandle.WaitTimeout)) { // Change thread's state to runnable: Runpolicy will decide latter on // if it can actually run the thread entry.Thread.ChangeSchedulerState(ThreadState.Runnable); // Place thread at the tail of the unblocked queue this.unblockedThreads.EnqueueTail(entry); } } } // Adjust the timeout so that we wake up sooner if // there is a thread waiting on a timer. if (!this.timerThreads.IsEmpty() && this.timerThreads.Head.Thread.blockedUntil < (now + this.minSlice)) { this.nextTimer = this.timerThreads.Head.Thread.blockedUntil; } else { this.nextTimer = now + this.idleSlice; } // Remember new alarm we are to set delta = this.nextTimer - now; // We are done: Don't forget to release timer lock: timerLock.Release(); return delta; } /// /// /// Remove a timer from timer queue: if thread is still on a timer queue. Need to /// disable interrupts before acquire timer lock - dispatcher always calls into timer /// with interrupts disable. If we don't disable interrupts we will get into possible deadlock /// with dispatcher /// /// [CLSCompliant(false)] [NoHeapAllocation] [HalLock] private void RemoveTimer(Thread thread) { // For now disable interrupts and acquire scheduler lock bool shouldEnable = Processor.DisableInterrupts(); // Acquire timer lock timerLock.Acquire(); if (thread.timerEntry.queue != null) { this.timerThreads.Remove(thread.timerEntry); } // Release timer lock timerLock.Release(); // Reenable interrupts Processor.RestoreInterrupts(shouldEnable); } /// /// /// Park blocking thread in a timer queue with a given time out. Need to /// disable interrupts before acquiring timer lock - dispatcher always calls into timer /// with interrupts disable. If we don't disable interrupts we will get into deadlock /// /// /// Thread to block /// Period of time for a thread to wait before timeout /// [NoHeapAllocation] [HalLock] private void EnqueueTimer(Thread thread, SchedulerTime stop) { // For now disable interrupts and acquire scheduler lock... bool shouldEnable = Processor.DisableInterrupts(); int unblockedBy; SchedulerTime now = SchedulerTime.Now; TimeSpan delta = stop - now; //Acquire scheduler lock: this.timerLock.Acquire(); // Find out if we need to update alarm. bool shouldChangeAlarm = ((this.timerThreads.Head == null) || (this.timerThreads.Head.Thread.blockedUntil > stop)); // If new wait already expired just try to fail it right a way. Alarm upate // indicates that only this new wait can be retired if (shouldChangeAlarm && stop <= now) { unblockedBy = thread.Unblock(WaitHandle.WaitTimeout); // We no longer should be changing alarm shouldChangeAlarm = false; // if we unblocked by timer or by someone else we don't have to anything - // OnThreadBlock will adjust thread's state accordingly. For more info see // OnThreadBlock, Dispatcher.ContextSwitch and RunPolicy } else { // Enqueue thread in the right place ThreadEntry entry = this.timerThreads.Head; while (entry != null && entry.Thread.blockedUntil <= stop) { // Loop until we find the first thread with a later stop. entry = entry.Next; } // Store thread's block until information thread.blockedUntil = stop; // Found the right place go ahead and put the thread into the queue this.timerThreads.InsertBefore(entry, thread.timerEntry); } // Before we let others to party on a timer check if timer has to be reset: // and if so rember it and reset once we release the lock if (shouldChangeAlarm && this.nextTimer > stop) { this.nextTimer = stop; } else { shouldChangeAlarm = false; } // Release timer lock this.timerLock.Release(); // If we are required to set a time on dispatcher, do it outside of // spinlock since nobody is currently can be running on our processor // since we have interrupts disabled. if (shouldChangeAlarm) { Processor.CurrentProcessor.SetNextTimerInterrupt(delta); } // Re-enable interrupts Processor.RestoreInterrupts(shouldEnable); } /// /// /// Property to get to the scheduler runnable queue lock /// /// internal SchedulerLock MyLock { [Inline] [NoHeapAllocation] get { return this.runnableLock; } } /// /// /// Get the affinity of this base scheduler /// /// internal int Affinity { [Inline] [NoHeapAllocation] get { return this.affinity; } } /// A spinlock protecting state of the timer queue private SchedulerLock runnableLock; /// A spinlock protecting state of the timer queue private SpinLock timerLock; /// List of recently runnable, but unscheduled threads. private ThreadQueue unblockedThreads = new ThreadQueue(); /// List of runnable, but unscheduled threads. private ThreadQueue runnableThreads = new ThreadQueue(); /// List of blocked threads, sorted by wait time. private ThreadQueue timerThreads = new ThreadQueue(); /// Scheduler affinity private readonly int affinity; /// List of frozen threads (unsorted) private SchedulerTime nextTimer; /// Idle thread private Thread idleThread; /// Run time slice private TimeSpan minSlice; /// Run time for idle slice private TimeSpan idleSlice; /// Check if need to process interprocess interrupt private bool isIpiNeeded; } }