//****************************************************************************************************** // LogicalThreadScheduler.cs - Gbtc // // Copyright © 2015, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 09/23/2015 - Stephen C. Wills // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; namespace GSF.Threading { /// /// Manages synchronization of actions by dispatching actions /// to logical threads to be processed synchronously. /// public class LogicalThreadScheduler { #region [ Members ] // Events /// /// Triggered when an action managed by this /// synchronization manager throws an exception. /// public event EventHandler> UnhandledException; // Fields private ConcurrentQueue>[] m_logicalThreadQueues; private int m_maxThreadCount; private int m_threadCount; private bool m_useBackgroundThreads; #endregion #region [ Constructors ] /// /// Creates a new instance of the class. /// /// The number of levels of priority supported by this logical thread scheduler. /// is less than or equal to zero. public LogicalThreadScheduler(int priorityLevels = 1) { if (priorityLevels < 1) throw new ArgumentException($"A logical thread scheduler must have at least one priority level.", nameof(priorityLevels)); m_maxThreadCount = Environment.ProcessorCount; m_logicalThreadQueues = new ConcurrentQueue>[priorityLevels]; m_useBackgroundThreads = true; for (int i = 0; i < priorityLevels; i++) m_logicalThreadQueues[i] = new ConcurrentQueue>(); } #endregion #region [ Properties ] /// /// Gets or sets the target for the maximum number of physical /// threads managed by this synchronization manager at any given time. /// public int MaxThreadCount { get { return Interlocked.CompareExchange(ref m_maxThreadCount, 0, 0); } set { if (value <= 0) throw new ArgumentException("Max thread count must be greater than zero", nameof(value)); int diff = value - m_maxThreadCount; int inactiveThreads = Math.Min(diff, m_logicalThreadQueues.Sum(queue => queue.Count)); Interlocked.Exchange(ref m_maxThreadCount, value); for (int i = 0; i < inactiveThreads; i++) ActivatePhysicalThread(); } } /// /// Gets or sets the flag that determines whether the threads in /// the scheduler's thread pool should be background threads. /// public bool UseBackgroundThreads { get { return m_useBackgroundThreads; } set { m_useBackgroundThreads = value; } } /// /// Gets the number of levels of priority /// supported by this scheduler. /// public int PriorityLevels { get { return m_logicalThreadQueues.Length; } } /// /// Gets the current number of active physical threads. /// private int ThreadCount { get { return Interlocked.CompareExchange(ref m_threadCount, 0, 0); } } #endregion #region [ Methods ] /// /// Creates a new logical thread whose /// execution is managed by this scheduler. /// /// A new logical thread managed by this scheduler. public LogicalThread CreateThread() { return new LogicalThread(this); } /// /// Signals the manager when a logical /// thread has new actions to be processed. /// /// The thread with new actions to be processed. /// The priority at which the thread is being signaled. internal void SignalItemHandler(LogicalThread thread, int priority) { if (thread.TryActivate(priority)) { // Make sure the thread has an action to be processed because // another thread could have processed all remaining actions and // deactivated the thread right before the call to TryActivate() if (!thread.HasAction) { thread.Deactivate(); return; } Enqueue(thread); ActivatePhysicalThread(); } } /// /// Activates a new physical thread if the thread /// count has not yet reached its maximum limit. /// private void ActivatePhysicalThread() { int threadCount; int newThreadCount; threadCount = ThreadCount; while (threadCount < Interlocked.CompareExchange(ref m_maxThreadCount, 0, 0)) { newThreadCount = Interlocked.CompareExchange(ref m_threadCount, threadCount + 1, threadCount); if (newThreadCount == threadCount) { StartNewPhysicalThread(); break; } threadCount = newThreadCount; } } /// /// Starts a new physical thread to /// process actions from logical threads. /// private void StartNewPhysicalThread() { Thread thread = new Thread(ProcessLogicalThreads); thread.IsBackground = m_useBackgroundThreads; thread.Start(); } /// /// Processes the next available task from the least recently /// processed logical thread that is available for processing. /// private void ProcessLogicalThreads() { Stopwatch stopwatch; Func accessor = null; LogicalThread thread; Action action; stopwatch = new Stopwatch(); while (ThreadCount <= MaxThreadCount && (object)m_logicalThreadQueues.FirstOrDefault(queue => queue.TryDequeue(out accessor)) != null) { thread = accessor(); if ((object)thread == null) continue; action = thread.Pull(); if ((object)action != null) { LogicalThread.CurrentThread = thread; stopwatch.Restart(); TryExecute(action); stopwatch.Stop(); LogicalThread.CurrentThread = null; thread.UpdateStatistics(stopwatch.Elapsed); } if (thread.HasAction) Enqueue(thread); else thread.Deactivate(); } DeactivatePhysicalThread(); if (!m_logicalThreadQueues.All(queue => queue.IsEmpty)) ActivatePhysicalThread(); } /// /// Queues the given thread for execution. /// /// The thread to be queued for execution. private void Enqueue(LogicalThread thread) { ICancellationToken executionToken; int activePriority; int nextPriority; do { // Create the execution token to be used in the closure ICancellationToken nextExecutionToken = new CancellationToken(); // Always update the thread's active priority before // the execution token to mitigate race conditions nextPriority = thread.NextPriority; thread.ActivePriority = nextPriority; thread.NextExecutionToken = nextExecutionToken; // Now that the action can be cancelled by another thread using the // new cancellation token, it should be safe to put it in the queue m_logicalThreadQueues[PriorityLevels - nextPriority].Enqueue(() => { if (nextExecutionToken.Cancel()) return thread; return null; }); // Because enqueuing the thread is a multi-step process, we need to // double-check in case the thread's priority changed in the meantime activePriority = thread.ActivePriority; nextPriority = thread.NextPriority; // We can use the cancellation token we just created because we only // really need to double-check the work that was done on this thread; // in other words, if another thread changed the priority in the // meantime, it can double-check its own work executionToken = nextExecutionToken; } while (activePriority != nextPriority && executionToken.Cancel()); } /// /// Attempts to execute the given action. /// /// The action to be executed. private void TryExecute(Action action) { try { action(); } catch (Exception ex) { if (!TryHandleException(ex)) throw; } } /// /// Decrements the thread count upon deactivation of a physical thread. /// private void DeactivatePhysicalThread() { Interlocked.Decrement(ref m_threadCount); } /// /// Attempts to handle the exception via either the logical thread's /// exception handler or the scheduler's exception handler. /// /// The unhandled exception thrown by an action on the logical thread. /// True if the exception could be handled; false otherwise. private bool TryHandleException(Exception unhandledException) { AggregateException aggregateException; StringBuilder message; bool handled; message = new StringBuilder(); message.AppendFormat("Logical thread action threw an exception of type {0}: {1}", unhandledException.GetType().FullName, unhandledException.Message); aggregateException = new AggregateException(message.ToString(), unhandledException); try { // Attempt to handle the exception via the logical thread's exception handler handled = LogicalThread.CurrentThread.OnUnhandledException(unhandledException); } catch (Exception handlerException) { // If the handler throws an exception, // make a note of it in the exception's exception message message.AppendLine(); message.AppendFormat("Logical thread exception handler threw an exception of type {0}: {1}", handlerException.GetType().FullName, handlerException.Message); aggregateException = new AggregateException(message.ToString(), aggregateException.InnerExceptions.Concat(new Exception[] { handlerException })); handled = false; } try { // If the logical thread's exception handler was not able to handle the exception, // attempt to handle the exception via the thread scheduler's exception handler Exception ex = (aggregateException.InnerExceptions.Count > 1) ? aggregateException : unhandledException; handled = handled || OnUnhandledException(ex); } catch (Exception handlerException) { // If the handler throws an exception, // make a note of it in the exception's exception message message.AppendLine(); message.AppendFormat("Scheduler exception handler threw an exception of type {0}: {1}", handlerException.GetType().FullName, handlerException.Message); aggregateException = new AggregateException(message.ToString(), aggregateException.InnerExceptions.Concat(new Exception[] { handlerException })); handled = false; } if (!handled) { // If the exception could not be handled by either the // logical thread's exception handler or the scheduler's // exception handler, throw it as an unhandled exception if (aggregateException.InnerExceptions.Count > 1) throw aggregateException; return false; } return true; } /// /// Raises the event. /// /// The unhandled exception. /// True if there are any handlers attached to this event; false otherwise. private bool OnUnhandledException(Exception ex) { EventHandler> unhandledException = UnhandledException; if ((object)unhandledException == null) return false; unhandledException(this, new EventArgs(ex)); return true; } #endregion } }