#region Apache License 2.0 /* Nuclex .NET Framework Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #endregion // Apache License 2.0 #if !NO_CONCURRENT_COLLECTIONS using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace Nuclex.Support.Threading { /// Executes actions in a threads public abstract class ThreadRunner : IDisposable { #region interface IRunner /// Interface for a background task runner private interface IRunner { /// Runs the background task void Run(); /// The runner's cancellation token source CancellationTokenSource CancellationTokenSource { get; } } #endregion // interface IRunner #region struct Runner /// Runs a background task with no parameters private struct Runner : IRunner { /// Initializes a new runner /// Action the runner will execute public Runner(Action action) { this.action = action; } /// Executes the runner's action public void Run() { this.action(); } /// The runner's cancellation token source public CancellationTokenSource CancellationTokenSource { get { return null; } } /// Action the runner will execute private Action action; } #endregion // struct Runner #region struct CancellableRunner /// Runs a background task with no parameters private struct CancellableRunner : IRunner { /// Initializes a new runner /// Action the runner will execute public CancellableRunner(CancellableAction action) { this.action = action; this.cancellationTokenSource = new CancellationTokenSource(); } /// Executes the runner's action public void Run() { this.action(this.cancellationTokenSource.Token); } /// The runner's cancellation token source public CancellationTokenSource CancellationTokenSource { get { return this.cancellationTokenSource; } } /// The runner's cancellation token source private CancellationTokenSource cancellationTokenSource; /// Action the runner will execute private CancellableAction action; } #endregion // struct CancellableRunner #region struct Runner /// Runs a background task with one parameter private struct Runner : IRunner { /// Initializes a new runner /// Action the runner will execute /// Parameter that will be passed to the action public Runner(Action action, P1 parameter1) { this.action = action; this.parameter1 = parameter1; } /// Executes the runner's action public void Run() { this.action(this.parameter1); } /// The runner's cancellation token source public CancellationTokenSource CancellationTokenSource { get { return null; } } /// Action the runner will execute private Action action; /// Parameter that will be passed to the action private P1 parameter1; } #endregion // struct Runner #region struct CancellableRunner /// Runs a background task with one parameter private struct CancellableRunner : IRunner { /// Initializes a new runner /// Action the runner will execute /// Parameter that will be passed to the action public CancellableRunner(CancellableAction action, P1 parameter1) { this.action = action; this.parameter1 = parameter1; this.cancellationTokenSource = new CancellationTokenSource(); } /// Executes the runner's action public void Run() { this.action(this.parameter1, this.cancellationTokenSource.Token); } /// The runner's cancellation token source public CancellationTokenSource CancellationTokenSource { get { return this.cancellationTokenSource; } } /// The runner's cancellation token source private CancellationTokenSource cancellationTokenSource; /// Action the runner will execute private CancellableAction action; /// Parameter that will be passed to the action private P1 parameter1; } #endregion // struct CancellableRunner #region struct Runner /// Runs a background task with one parameter private struct Runner : IRunner { /// Initializes a new runner /// Action the runner will execute /// First parameter that will be passed to the action /// Second parameter that will be passed to the action public Runner(Action action, P1 parameter1, P2 parameter2) { this.action = action; this.parameter1 = parameter1; this.parameter2 = parameter2; } /// Executes the runner's action public void Run() { this.action(this.parameter1, this.parameter2); } /// The runner's cancellation token source public CancellationTokenSource CancellationTokenSource { get { return null; } } /// Action the runner will execute private Action action; /// First parameter that will be passed to the action private P1 parameter1; /// Second parameter that will be passed to the action private P2 parameter2; } #endregion // struct Runner #region struct CancellableRunner /// Runs a background task with one parameter private struct CancellableRunner : IRunner { /// Initializes a new runner /// Action the runner will execute /// First parameter that will be passed to the action /// Second parameter that will be passed to the action public CancellableRunner(CancellableAction action, P1 parameter1, P2 parameter2) { this.action = action; this.parameter1 = parameter1; this.parameter2 = parameter2; this.cancellationTokenSource = new CancellationTokenSource(); } /// Executes the runner's action public void Run() { this.action(this.parameter1, this.parameter2, this.cancellationTokenSource.Token); } /// The runner's cancellation token source public CancellationTokenSource CancellationTokenSource { get { return this.cancellationTokenSource; } } /// The runner's cancellation token source private CancellationTokenSource cancellationTokenSource; /// Action the runner will execute private CancellableAction action; /// First parameter that will be passed to the action private P1 parameter1; /// Second parameter that will be passed to the action private P2 parameter2; } #endregion // struct CancellableRunner /// Initializes a new background processing handler public ThreadRunner() { this.executeQueuedRunnersInThreadDelegate = new Action(executeQueuedRunnersInThread); this.queuedRunners = new ConcurrentQueue(); } /// /// Immediately cancels all operations and releases the resources used by the instance /// public void Dispose() { Dispose(timeoutMilliseconds: 2500); } /// /// Immediately cancels all operations and releases the resources used by the instance /// /// /// Time to wait for the background tasks before dropping the tasks unfinished /// public void Dispose(int timeoutMilliseconds) { CancelAllBackgroundOperations(); Task currentTask; lock(this) { currentTask = this.currentTask; } if(currentTask != null) { if(!currentTask.Wait(timeoutMilliseconds)) { Debug.Assert(false, "Task does not support cancellation or did not cancel in time"); } lock(this) { this.currentTask = null; IsBusy = false; } } } /// Whether the view model is currently busy executing a task public bool IsBusy { get { return this.isBusy; } private set { if(value != this.isBusy) { this.isBusy = value; BusyChanged(); } } } /// Reports an error /// Error that will be reported protected abstract void ReportError(Exception exception); /// Called when the status of the busy flag changes protected abstract void BusyChanged(); /// Executes the specified operation in the background /// Action that will be executed in the background public void RunInBackground(Action action) { this.queuedRunners.Enqueue(new Runner(action)); startBackgroundProcessingIfNecessary(); } /// Executes the specified operation in the background /// Action that will be executed in the background public void RunInBackground(CancellableAction action) { this.queuedRunners.Enqueue(new CancellableRunner(action)); startBackgroundProcessingIfNecessary(); } /// Executes the specified operation in the background /// Action that will be executed in the background /// Parameter that will be passed to the action public void RunInBackground(Action action, P1 parameter1) { this.queuedRunners.Enqueue(new Runner(action, parameter1)); startBackgroundProcessingIfNecessary(); } /// Executes the specified operation in the background /// Action that will be executed in the background /// Parameter that will be passed to the action public void RunInBackground(CancellableAction action, P1 parameter1) { this.queuedRunners.Enqueue(new CancellableRunner(action, parameter1)); startBackgroundProcessingIfNecessary(); } /// Executes the specified operation in the background /// Action that will be executed in the background /// First parameter that will be passed to the action /// Second parameter that will be passed to the action public void RunInBackground(Action action, P1 parameter1, P2 parameter2) { this.queuedRunners.Enqueue(new Runner(action, parameter1, parameter2)); startBackgroundProcessingIfNecessary(); } /// Executes the specified operation in the background /// Action that will be executed in the background /// First parameter that will be passed to the action /// Second parameter that will be passed to the action public void RunInBackground( CancellableAction action, P1 parameter1, P2 parameter2 ) { this.queuedRunners.Enqueue(new CancellableRunner(action, parameter1, parameter2)); startBackgroundProcessingIfNecessary(); } /// Cancels the currently running background operation public void CancelBackgroundOperation() { IRunner currentRunner = this.currentRunner; if(currentRunner != null) { CancellationTokenSource cancellationTokenSource = currentRunner.CancellationTokenSource; if(cancellationTokenSource != null) { cancellationTokenSource.Cancel(); } } } /// Cancels all queued and the currently running background operation public void CancelAllBackgroundOperations() { IRunner runner; while(this.queuedRunners.TryDequeue(out runner)) { CancellationTokenSource cancellationTokenSource = runner.CancellationTokenSource; if(cancellationTokenSource != null) { cancellationTokenSource.Cancel(); } } CancelBackgroundOperation(); } /// Whether the background operation has been cancelled //[Obsolete("Please use a method accepting a cancellation token instead of using this")] public bool IsBackgroundOperationCancelled { get { IRunner currentRunner = this.currentRunner; if(currentRunner != null) { return currentRunner.CancellationTokenSource.IsCancellationRequested; } else { return false; } } } /// Throws an exception if the background operation was cancelled //[Obsolete("Please use a method accepting a cancellation token instead of using this")] public void ThrowIfBackgroundOperationCancelled() { IRunner currentRunner = this.currentRunner; if(currentRunner != null) { CancellationTokenSource source = currentRunner.CancellationTokenSource; if(source != null) { source.Token.ThrowIfCancellationRequested(); } } } /// Executes the queued runners in the background private void executeQueuedRunnersInThread() { IsBusy = true; IRunner runner; while(this.queuedRunners.TryDequeue(out runner)) { try { this.currentRunner = runner; runner.Run(); } catch(OperationCanceledException) { // Ignore } catch(Exception exception) { this.currentRunner = null; // When the error is reported this should already be null ReportError(exception); } this.currentRunner = null; } lock(this) { this.currentTask = null; IsBusy = false; } } /// Starts the background processing thread, if needed private void startBackgroundProcessingIfNecessary() { Task currentTask; lock(this) { if(this.currentTask == null) { currentTask = new Task(this.executeQueuedRunnersInThreadDelegate); this.currentTask = currentTask; } else { return; // Task is already running } } // Start the task outside of the lock statement so that when the thread starts to run, // it is guaranteed to read the currentTask variable as the task we just created. currentTask.Start(); } /// Whether the view model is currently busy executing a task private volatile bool isBusy; /// Delegate for the executedQueuedRunnersInThread() method private Action executeQueuedRunnersInThreadDelegate; /// Queued background operations private ConcurrentQueue queuedRunners; /// Runner currently executing in the background private volatile IRunner currentRunner; /// Task that is currently executing the runners private Task currentTask; } } // namespace Nuclex.Support.Threading #endif // !NO_CONCURRENT_COLLECTIONS