#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