#region CPL License /* Nuclex Framework Copyright (C) 2002-2019 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 #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) { currentRunner.CancellationTokenSource.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