//******************************************************************************************************
// LogicalThreadOperation.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/24/2015 - Stephen C. Wills
// Generated original version of source code.
// 02/16/2016 - J. Ritchie Carroll
// Corrected priority behavior and added override feature, made explicit RunIfPending optional
//
//******************************************************************************************************
using System;
using System.Threading;
namespace GSF.Threading
{
///
/// Synchronized operation that executes on a logical thread.
///
///
/// This synchronized operation optionally supports a different usage pattern which will
/// allow for asynchronous loops and signals to be passed between threads throughout an
/// operation. The following is an example of how to implement a simple asynchronous loop
/// between two threads using this class. Note that when used, the pattern requires more
/// diligence on the user's part for handling exceptions and signaling when the operation
/// is complete:
///
///
/// LogicalThread thread1 = new LogicalThread();
/// LogicalThread thread2 = new LogicalThread();
///
/// // Create logical thread operation with manually controlled call to RunIfPending
/// LogicalThreadOperation operation = new LogicalThreadOperation(thread1, DoOperation, false);
///
/// private void DoOperation()
/// {
/// ExecuteOnThread1();
/// thread2.Push(() => operation.ExecuteAction(() =>
/// {
/// ExecuteOnThread2();
/// operation.RunIfPending();
/// }));
/// });
///
///
public class LogicalThreadOperation
{
#region [ Members ]
// Constants
private const int NotRunning = 0;
private const int Running = 1;
private const int Pending = 2;
// Fields
private readonly LogicalThread m_thread;
private readonly Action m_action;
private readonly bool m_autoRunIfPending;
private int m_priority;
private int m_state;
private int m_queuedPriority;
private CancellationToken m_cancellationToken;
#endregion
#region [ Constructors ]
///
/// Creates a new instance of the class.
///
/// The thread on which to execute the operation's action.
/// The action to be executed.
///
/// Set to true to execute automatically; otherwise,
/// set to false for user controlled call timing.
///
public LogicalThreadOperation(LogicalThread thread, Action action, bool autoRunIfPending = true)
: this(thread, action, 1, autoRunIfPending)
{
}
///
/// Creates a new instance of the class.
///
/// The thread on which to execute the operation's action.
/// The action to be executed.
/// The priority with which the action should be executed on the logical thread.
///
/// Set to true to execute automatically; otherwise,
/// set to false for user controlled call timing.
///
/// is outside the range between 1 and .
public LogicalThreadOperation(LogicalThread thread, Action action, int priority, bool autoRunIfPending = true)
{
m_thread = thread;
m_action = action;
Priority = priority;
m_autoRunIfPending = autoRunIfPending;
// Initialize this class with a cancelled token so that
// calls to EnsurePriority before the first call to
// ExecuteActionAsync do not inadvertently queue actions
m_cancellationToken = new CancellationToken();
m_cancellationToken.Cancel();
}
#endregion
#region [ Properties ]
///
/// Gets a value to indicate whether the operation is currently executing actions.
///
public bool IsRunning => Interlocked.CompareExchange(ref m_state, NotRunning, NotRunning) != NotRunning;
///
/// Gets a value to indicate whether the operation has an additional operation
/// that is pending execution after the currently running operation has completed.
///
public bool IsPending => Interlocked.CompareExchange(ref m_state, NotRunning, NotRunning) == Pending;
///
/// Gets flag that determines if will be called automatically.
///
public bool AutoRunIfPending => m_autoRunIfPending;
///
/// Gets or sets default priority for logical thread operation.
///
/// is outside the range between 1 and .
///
/// Updates to default priority will only take effect during next call.
///
public int Priority
{
get
{
return Interlocked.CompareExchange(ref m_priority, 0, 0);
}
set
{
if (value < 1 || value > m_thread.PriorityLevels)
throw new ArgumentException($"Logical thread does not support priority level {value}. Specify a priority between 1 and {m_thread.PriorityLevels}.", nameof(value));
Interlocked.Exchange(ref m_priority, value);
}
}
#endregion
#region [ Methods ]
///
/// Executes the action on the current thread or marks the operation as
/// pending if the operation is already running.
///
///
/// When the operation is marked as pending, it will run again at the next
/// call to . This can be useful if an update to
/// an object's state has invalidated the operation that is currently running
/// and will therefore need to be run again.
///
public void RunOnce()
{
// if (m_state == NotRunning)
// TryRunOnce();
// else if (m_state == Running)
// m_state = Pending;
if (Interlocked.CompareExchange(ref m_state, Pending, Running) == NotRunning)
TryRunOnce();
}
///
/// Executes the action on another thread or marks the operation as pending
/// if the operation is already running.
///
///
/// When the operation is marked as pending, it will run again at the next
/// call to . This can be useful if an update to
/// an object's state has invalidated the operation that is currently running
/// and will therefore need to be run again.
///
public void RunOnceAsync()
{
// if (m_state == NotRunning)
// TryRunOnceAsync();
// else if (m_state == Running)
// m_state = Pending;
if (Interlocked.CompareExchange(ref m_state, Pending, Running) == NotRunning)
TryRunOnceAsync();
}
///
/// Attempts to execute the action on the current thread.
/// Does nothing if the operation is already running.
///
public void TryRunOnce()
{
// if (m_state == NotRunning)
// {
// m_state = Running;
//
// if (ExecuteAction())
// ExecuteActionAsync();
// }
if (Interlocked.CompareExchange(ref m_state, Running, NotRunning) == NotRunning)
ExecuteAction(m_action);
}
///
/// Attempts to execute the action on another thread.
/// Does nothing if the operation is already running.
///
public void TryRunOnceAsync()
{
// if (m_state == NotRunning)
// {
// m_state = Running;
// ExecuteActionAsync();
// }
if (Interlocked.CompareExchange(ref m_state, Running, NotRunning) == NotRunning)
ExecuteActionAsync(Priority);
}
///
/// Starts the operation over at the beginning if the operation is pending or sets
/// the operation state back to not running so it can run again.
///
///
/// This method must be called at the end of an operation in order to set the state
/// of the operation back to running or not running so that the operation can run again.
/// The existence of this method makes this implementation different from other synchronized
/// operations in that it requires more diligence on the user's part to signal when the
/// operation is complete. In turn, this allows the user to implement complex operations
/// that may involve asynchronous loops and signaling patterns that would not be possible
/// with the interface.
///
public void RunIfPending()
{
// if (m_state == Pending)
// {
// m_state = Running;
// return true;
// }
// else if (m_state == Running)
// {
// m_state = NotRunning;
// }
if (Interlocked.CompareExchange(ref m_state, NotRunning, Running) == Pending)
{
// There is no race condition here because if m_state is Pending,
// then it cannot be changed by any other line of code except this one
Interlocked.Exchange(ref m_state, Running);
ExecuteActionAsync(Priority);
}
}
///
/// Executes an action once on the current thread.
///
/// to run on current thread.
///
/// This method provides exception handling for the action passed into this
/// method with a couple of guarantees. The first is that regardless of what
/// thread is executing the action passed into this method, the exception
/// will be raised on the thread that the logical operation runs on. The
/// second is that the RunIfPending method will be called if an exception
/// does occur in the given action.
///
public void ExecuteAction(Action action)
{
try
{
action();
if (m_autoRunIfPending)
RunIfPending();
}
catch (Exception ex)
{
if (LogicalThread.CurrentThread != m_thread)
{
m_thread.Push(Priority, () =>
{
string message = $"Exception occurred while executing logical thread operation: {ex.Message}";
throw new Exception(message, ex);
});
}
RunIfPending();
if (LogicalThread.CurrentThread == m_thread)
throw;
}
}
///
/// If the operation is running, the action has yet to be executed,
/// and the given priority level differs from the queued action's
/// priority level, this method will requeue the action at the given
/// priority level.
///
/// The priority at which the current operation should be requeued.
public void RequeueAction(int priority)
{
CancellationToken cancellationToken;
int queuedPriority;
// Order of operations here is vital to avoid cancelling freshly
// queued operations when the user hasn't changed the priority level
cancellationToken = Interlocked.CompareExchange(ref m_cancellationToken, null, null);
queuedPriority = Interlocked.CompareExchange(ref m_queuedPriority, 0, 0);
// If the priority has changed, attempt to cancel the currently queued action.
// If the action was not previously cancelled, requeue the action at the given priority
if (queuedPriority != priority && cancellationToken.Cancel())
ExecuteActionAsync(priority);
}
private void ExecuteActionAsync(int priority)
{
CancellationToken cancellationToken = new CancellationToken();
// Order of operations here is vital to avoid getting
// cancelled when the user hasn't changed the priority level
Interlocked.Exchange(ref m_queuedPriority, priority);
Interlocked.Exchange(ref m_cancellationToken, cancellationToken);
m_thread.Push(priority, () =>
{
// If the cancellation token was previously cancelled,
// it means this action has been requeued so don't do anything.
// By cancelling it now, we let requeue operations know that the
// action is currently executing and will soon be requeued anyway
if (m_cancellationToken.Cancel())
ExecuteAction(m_action);
});
}
#endregion
}
}