From 2f82a2fdf9a1449733179fee95c1e4280a47578c Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Sat, 2 Feb 2019 09:12:00 +0000 Subject: [PATCH] Updated Mono project (may replace it with pure SCons or Gradle build soon); added a threaded task runner I wrote for some WPF applications going through MCSE; wrote more unit tests for the thread runner git-svn-id: file:///srv/devel/repo-conversion/nusu@332 d2e56fa2-650e-0410-a79f-9358c0239efd --- Nuclex.Support (mono-3.5).csproj | 28 +- Nuclex.Support (net-4.0).csproj | 5 + Source/Threading/CancellableAction.cs | 49 +++ Source/Threading/ThreadRunner.Test.cs | 460 +++++++++++++++++++++++++ Source/Threading/ThreadRunner.cs | 461 ++++++++++++++++++++++++++ 5 files changed, 995 insertions(+), 8 deletions(-) create mode 100644 Source/Threading/CancellableAction.cs create mode 100644 Source/Threading/ThreadRunner.Test.cs create mode 100644 Source/Threading/ThreadRunner.cs diff --git a/Nuclex.Support (mono-3.5).csproj b/Nuclex.Support (mono-3.5).csproj index 66adcc3..26f0268 100644 --- a/Nuclex.Support (mono-3.5).csproj +++ b/Nuclex.Support (mono-3.5).csproj @@ -48,6 +48,13 @@ + + + + + + + @@ -235,10 +242,6 @@ CommandLine.cs - - - ParallelBackgroundWorker.cs - ParserHelper.cs @@ -247,10 +250,6 @@ PropertyChangedEventArgsHelper.cs - - - AffineThreadPool.cs - EnumHelper.cs @@ -263,6 +262,19 @@ ObservableHelper.cs + + + AffineThreadPool.cs + + + + + ParallelBackgroundWorker.cs + + + + ThreadRunner.cs + TypeHelper.cs diff --git a/Nuclex.Support (net-4.0).csproj b/Nuclex.Support (net-4.0).csproj index 2ce6430..2231bc5 100644 --- a/Nuclex.Support (net-4.0).csproj +++ b/Nuclex.Support (net-4.0).csproj @@ -275,10 +275,15 @@ AffineThreadPool.cs + ParallelBackgroundWorker.cs + + + ThreadRunner.cs + TypeHelper.cs diff --git a/Source/Threading/CancellableAction.cs b/Source/Threading/CancellableAction.cs new file mode 100644 index 0000000..12fd452 --- /dev/null +++ b/Source/Threading/CancellableAction.cs @@ -0,0 +1,49 @@ +#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 + +using System; +using System.Threading; + +namespace Nuclex.Support.Threading { + + /// Action with no arguments that can be cancelled + /// + /// Cancellation token by which the action can be cancelled + /// + public delegate void CancellableAction(CancellationToken cancellationToken); + + /// Action with no arguments that can be cancelled + /// + /// Cancellation token by which the action can be cancelled + /// + /// First argument for the action + public delegate void CancellableAction(T1 arg1, CancellationToken cancellationToken); + + /// Action with no arguments that can be cancelled + /// + /// Cancellation token by which the action can be cancelled + /// + /// First argument for the action + /// Second argument for the action + public delegate void CancellableAction( + T1 arg1, T2 arg2, CancellationToken cancellationToken + ); + +} // namespace Nuclex.Support.Threading diff --git a/Source/Threading/ThreadRunner.Test.cs b/Source/Threading/ThreadRunner.Test.cs new file mode 100644 index 0000000..daa7e67 --- /dev/null +++ b/Source/Threading/ThreadRunner.Test.cs @@ -0,0 +1,460 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2017 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.Threading; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Threading { + + /// Unit Test for the thread runner class + [TestFixture] + internal class ThreadRunnerTest { + + #region class DefaultDisposeRunner + + /// Implementation of a thread runner to check default dispose behavior + private class DefaultDisposeRunner : ThreadRunner { + + /// Reports an error + /// Error that will be reported + protected override void ReportError(Exception exception) { } + + /// Called when the status of the busy flag changes + protected override void BusyChanged() { } + + } + + #endregion // class DefaultDisposeRunner + + #region class DummyRunner + + /// Implementation of a thread runner used for unit testing + private class DummyRunner : ThreadRunner { + + /// Initializes a new dummy thread runner + public DummyRunner() : base() { + this.completionGate = new ManualResetEvent(initialState: false); + } + + /// Immediately frees all resources used by the instance + public new void Dispose() { + base.Dispose(100); + + if(this.completionGate != null) { + this.completionGate.Dispose(); + this.completionGate = null; + } + } + + /// Waits for the task for complete (all of 100 milliseconds) + /// True if the task completed, false if it continues running + public bool WaitForCompletion() { + return this.completionGate.WaitOne(100); + } + + /// How often the status of the busy flag has changed + public int BusyChangeCount { + get { return this.busyChangeCount; } + } + + /// Error that has been reported the last time a task was run + public Exception ReportedError { + get { return this.reportedError; } + } + + /// Reports an error + /// Error that will be reported + protected override void ReportError(Exception exception) { + this.reportedError = exception; + } + + /// Called when the status of the busy flag changes + protected override void BusyChanged() { + ++busyChangeCount; + if((busyChangeCount >= 2) && (base.IsBusy == false)) { + this.completionGate.Set(); + } + } + + /// Last error that was reported in the thread + private Exception reportedError; + /// Number of times the busy state of the runner has changed + private int busyChangeCount; + /// Triggered when the busy event has performed a double flank + private ManualResetEvent completionGate; + + } + + #endregion // class DummyRunner + + #region class DummyTask + + /// Dummy task that can be executed by a thread runner + private class DummyTask : IDisposable { + + /// Initializes a new dummy task + /// How long the task shoudl take to execute + public DummyTask(int delayMilliseconds) { + this.startGate = new ManualResetEvent(initialState: false); + this.delayMilliseconds = delayMilliseconds; + } + + /// Immediately releases all resources owned by the instance + public void Dispose() { + if(this.startGate != null) { + this.startGate.Dispose(); + this.startGate = null; + } + } + + /// Waits for the task to start (all of 100 milliseconds) + /// True if the start started, false if it didn't + public bool WaitForStart() { + return this.startGate.WaitOne(100); + } + + /// Sets the task up to fail with the specified error + /// Error the task will fail with + public void FailWith(Exception error) { + this.error = error; + } + + /// Runs the task with no arguments + public void Run() { + this.startGate.Set(); + + ++this.executionCount; + Thread.Sleep(this.delayMilliseconds); + if(this.error != null) { + throw this.error; + } + } + + /// Runs the task with one argument + /// First argument passed from the runner + public void Run(float firstArgument) { + this.startGate.Set(); + + ++this.executionCount; + this.firstArgument = firstArgument; + Thread.Sleep(this.delayMilliseconds); + if(this.error != null) { + throw this.error; + } + } + + /// Runs the task with two argument + /// First argument passed from the runner + /// Second argument passed from the runner + public void Run(float firstArgument, string secondArgument) { + this.startGate.Set(); + + ++this.executionCount; + this.firstArgument = firstArgument; + this.secondArgument = secondArgument; + Thread.Sleep(this.delayMilliseconds); + if(this.error != null) { + throw this.error; + } + } + + /// Runs the task with no arguments + /// Token by which cancellation can be signalled + public void RunCancellable(CancellationToken cancellationToken) { + this.startGate.Set(); + + ++this.executionCount; + + if(delayMilliseconds == 0) { + Thread.Sleep(0); + } else { + if(cancellationToken.WaitHandle.WaitOne(delayMilliseconds)) { + this.wasCancelled = cancellationToken.IsCancellationRequested; + cancellationToken.ThrowIfCancellationRequested(); + } + } + if(this.error != null) { + throw this.error; + } + } + + /// Runs the task with one argument + /// First argument passed from the runner + /// Token by which cancellation can be signalled + public void RunCancellable(float firstArgument, CancellationToken cancellationToken) { + this.startGate.Set(); + + ++this.executionCount; + this.firstArgument = firstArgument; + + if(delayMilliseconds == 0) { + Thread.Sleep(0); + } else { + if(cancellationToken.WaitHandle.WaitOne(delayMilliseconds)) { + this.wasCancelled = cancellationToken.IsCancellationRequested; + cancellationToken.ThrowIfCancellationRequested(); + } + } + if(this.error != null) { + throw this.error; + } + } + + /// Runs the task with two argument + /// First argument passed from the runner + /// Second argument passed from the runner + /// Token by which cancellation can be signalled + public void RunCancellable( + float firstArgument, string secondArgument, CancellationToken cancellationToken + ) { + this.startGate.Set(); + + ++this.executionCount; + this.firstArgument = firstArgument; + this.secondArgument = secondArgument; + + if(delayMilliseconds == 0) { + Thread.Sleep(0); + } else { + if(cancellationToken.WaitHandle.WaitOne(delayMilliseconds)) { + this.wasCancelled = cancellationToken.IsCancellationRequested; + cancellationToken.ThrowIfCancellationRequested(); + } + } + if(this.error != null) { + throw this.error; + } + } + + /// How many times the task was run + public int ExecutionCount { + get { return this.executionCount; } + } + + /// Whether the task was cancelled by the runner itself + public bool WasCancelled { + get { return this.wasCancelled; } + } + + /// What the first argument was during the last call + public float FirstArgument { + get { return this.firstArgument; } + } + + /// What the second argument was during the last call + public string SecondArgument { + get { return this.secondArgument; } + } + + /// Last error that was reported in the thread + private Exception error; + /// Triggered when the task has started + private ManualResetEvent startGate; + /// How long the task should take to execute in milliseconds + private int delayMilliseconds; + /// How many times the task has been executed + private volatile int executionCount; + /// Whether the task has been cancelled + private volatile bool wasCancelled; + /// First argument that was passed to the task + private volatile float firstArgument; + /// Second argument that was passed to the task + private volatile string secondArgument; + + } + + #endregion // class DummyRunner + + /// Verifies that the thread runner has a default constructor + [Test] + public void CanBeDefaultConstructed() { + using(new DummyRunner()) { } + } + + /// Checks that the runner sets and unsets its busy flag + [Test] + public void BusyFlagIsToggled() { + using(var runner = new DummyRunner()) { + int busyFlagChangeCount = runner.BusyChangeCount; + Assert.IsFalse(runner.IsBusy); + + runner.RunInBackground((Action)delegate() { }); + Assert.IsTrue(runner.WaitForCompletion()); + + Assert.GreaterOrEqual(busyFlagChangeCount + 2, runner.BusyChangeCount); + Assert.IsFalse(runner.IsBusy); + } + } + + /// Lets the thread runner run a simple task in the background + [Test] + public void CanRunSimpleTaskInBackground() { + using(var task = new DummyTask(0)) { + using(var runner = new DummyRunner()) { + runner.RunInBackground(new Action(task.Run)); + + Assert.IsTrue(runner.WaitForCompletion()); + Assert.IsNull(runner.ReportedError); + } + + Assert.AreEqual(1, task.ExecutionCount); + Assert.IsFalse(task.WasCancelled); + } + } + + /// + /// Checks that the thread runner is able to pass a single argument to a task + /// + [Test] + public void CanPassSingleArgumentToSimpleTask() { + using(var task = new DummyTask(0)) { + using(var runner = new DummyRunner()) { + runner.RunInBackground(new Action(task.Run), 12.43f); + + Assert.IsTrue(runner.WaitForCompletion()); + Assert.IsNull(runner.ReportedError); + } + + Assert.AreEqual(1, task.ExecutionCount); + Assert.AreEqual(12.43f, task.FirstArgument); + Assert.IsFalse(task.WasCancelled); + } + } + + /// + /// Checks that the thread runner is able to pass two arguments to a task + /// + [Test] + public void CanPassTwoArgumentsToSimpleTask() { + using(var task = new DummyTask(0)) { + using(var runner = new DummyRunner()) { + runner.RunInBackground(new Action(task.Run), 98.67f, "Hello"); + + Assert.IsTrue(runner.WaitForCompletion()); + Assert.IsNull(runner.ReportedError); + } + + Assert.AreEqual(1, task.ExecutionCount); + Assert.AreEqual(98.67f, task.FirstArgument); + Assert.AreEqual("Hello", task.SecondArgument); + Assert.IsFalse(task.WasCancelled); + } + } + + /// + /// Verifies that an error happening in a simple task is reported correctly + /// + [Test] + public void SimpleTaskErrorIsReported() { + using(var task = new DummyTask(0)) { + var error = new InvalidOperationException("Mooh!"); + task.FailWith(error); + + using(var runner = new DummyRunner()) { + runner.RunInBackground(new Action(task.Run)); + + Assert.IsTrue(runner.WaitForCompletion()); + Assert.AreSame(error, runner.ReportedError); + } + + Assert.AreEqual(1, task.ExecutionCount); + Assert.IsFalse(task.WasCancelled); + } + } + + /// Lets the thread runner run a cancellable task in the background + [Test] + public void CanRunCancellableTaskInBackground() { + using(var task = new DummyTask(100)) { + using(var runner = new DummyRunner()) { + runner.RunInBackground(new CancellableAction(task.RunCancellable)); + Assert.IsTrue(task.WaitForStart()); + runner.CancelAllBackgroundOperations(); + + Assert.IsTrue(runner.WaitForCompletion()); + Assert.IsNull(runner.ReportedError); + } + + Assert.AreEqual(1, task.ExecutionCount); + Assert.IsTrue(task.WasCancelled); + } + } + + /// + /// Checks that the thread runner is able to pass a single argument to a task + /// that can be cancelled + /// + [Test] + public void CanPassSingleArgumentToCancellableTask() { + using(var task = new DummyTask(100)) { + using(var runner = new DummyRunner()) { + runner.RunInBackground(new CancellableAction(task.RunCancellable), 12.43f); + Assert.IsTrue(task.WaitForStart()); + runner.CancelAllBackgroundOperations(); + + Assert.IsTrue(runner.WaitForCompletion()); + Assert.IsNull(runner.ReportedError); + } + + Assert.AreEqual(1, task.ExecutionCount); + Assert.AreEqual(12.43f, task.FirstArgument); + Assert.IsTrue(task.WasCancelled); + } + } + + /// + /// Checks that the thread runner is able to pass two arguments to a task + /// that can be cancelled + /// + [Test] + public void CanPassTwoArgumentsToCancellableTask() { + using(var task = new DummyTask(100)) { + using(var runner = new DummyRunner()) { + runner.RunInBackground( + new CancellableAction(task.RunCancellable), 98.67f, "Hello" + ); + Assert.IsTrue(task.WaitForStart()); + runner.CancelAllBackgroundOperations(); + + Assert.IsTrue(runner.WaitForCompletion()); + Assert.IsNull(runner.ReportedError); + } + + Assert.AreEqual(1, task.ExecutionCount); + Assert.AreEqual(98.67f, task.FirstArgument); + Assert.AreEqual("Hello", task.SecondArgument); + Assert.IsTrue(task.WasCancelled); + } + } + + } + +} // namespace Nuclex.Support.Threading + +#endif // UNITTEST + +#endif // !NO_CONCURRENT_COLLECTIONS diff --git a/Source/Threading/ThreadRunner.cs b/Source/Threading/ThreadRunner.cs new file mode 100644 index 0000000..748c254 --- /dev/null +++ b/Source/Threading/ThreadRunner.cs @@ -0,0 +1,461 @@ +#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 + internal 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