diff --git a/GrasshopperAsyncComponent/Base/GH_AsyncComponent.cs b/GrasshopperAsyncComponent/Base/GH_AsyncComponent.cs
index 92c9e6b..4bf3d06 100644
--- a/GrasshopperAsyncComponent/Base/GH_AsyncComponent.cs
+++ b/GrasshopperAsyncComponent/Base/GH_AsyncComponent.cs
@@ -8,6 +8,9 @@ using Timer = System.Timers.Timer;
namespace GrasshopperAsyncComponent
{
+ ///
+ /// Inherit your component from this class to make all the async goodness available.
+ ///
public abstract class GH_AsyncComponent : GH_Component
{
public override Guid ComponentGuid { get => new Guid("5DBBD498-0326-4E25-83A5-424D8DC493D4"); }
@@ -16,14 +19,6 @@ namespace GrasshopperAsyncComponent
public override GH_Exposure Exposure => GH_Exposure.hidden;
- public IAsyncComponentWorker Worker;
-
- IAsyncComponentWorker CurrentWorker;
-
- Task CurrentRun;
-
- ConcurrentBag TokenSources = new ConcurrentBag();
-
Action ReportProgress;
Action ReportError;
@@ -32,9 +27,27 @@ namespace GrasshopperAsyncComponent
Action Done;
+ Timer DisplayProgressTimer;
+
int State = 0;
- Timer DisplayProgressTimer;
+ bool SetData = false;
+
+ List Workers;
+
+ List Tasks;
+
+ List CancelationSources;
+
+ ///
+ /// Set this property inside the constructor of your derived component.
+ ///
+ public WorkerInstance BaseWorker { get; set; }
+
+ ///
+ /// Optional: if you have opinions on how the default system task scheduler should treat your workers, set it here.
+ ///
+ public TaskCreationOptions? TaskCreationOptions { get; set; } = null;
protected GH_AsyncComponent(string name, string nickname, string description, string category, string subCategory) : base(name, nickname, description, category, subCategory)
{
@@ -50,78 +63,127 @@ namespace GrasshopperAsyncComponent
ReportProgress = (progress) =>
{
- Rhino.RhinoApp.InvokeOnUiThread((Action)delegate
- {
- Message = progress;
- if (!DisplayProgressTimer.Enabled) DisplayProgressTimer.Start();
- });
+ Message = progress;
+ if (!DisplayProgressTimer.Enabled) DisplayProgressTimer.Start();
};
ReportError = (error, type) => Errors?.Add((error, type));
Done = () =>
{
- Rhino.RhinoApp.InvokeOnUiThread((Action)delegate
+ State++;
+
+ if (State == Workers.Count)
{
- State = 1;
- ExpireSolution(true);
- });
+ SetData = true;
+ // We need to reverse the workers list to set the outputs in the same order as the inputs.
+ Workers.Reverse();
+
+ Rhino.RhinoApp.InvokeOnUiThread((Action)delegate
+ {
+ ExpireSolution(true);
+ });
+ }
};
Errors = new List<(string, GH_RuntimeMessageLevel)>();
+
+ Workers = new List();
+ CancelationSources = new List();
+ Tasks = new List();
+ }
+
+ protected override void BeforeSolveInstance()
+ {
+ if (State != 0 && SetData) return;
+
+ foreach (var source in CancelationSources) source.Cancel();
+
+ CancelationSources.Clear();
+ Workers.Clear();
+ Errors.Clear();
+ Tasks.Clear();
+
+ State = 0;
+ }
+
+ protected override void AfterSolveInstance()
+ {
+ // We need to start all the tasks as close as possible to each other.
+ if (State == 0 && Tasks.Count > 0)
+ {
+ foreach (var task in Tasks) task.Start();
+ }
}
protected override void SolveInstance(IGH_DataAccess DA)
{
if (State == 0)
{
- if (Worker == null)
+ if (BaseWorker == null)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Worker class not provided.");
return;
}
- CurrentWorker = Worker.GetNewInstance();
+ var CurrentWorker = BaseWorker.Duplicate();
if (CurrentWorker == null)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Could not get a worker instance.");
return;
}
- Errors = new List<(string, GH_RuntimeMessageLevel)>();
-
- // Request the cancellation of any old tasks.
- CancellationTokenSource oldTokenSource;
- while (TokenSources.TryTake(out oldTokenSource))
- {
- oldTokenSource?.Cancel();
- }
-
// Let the worker collect data.
- CurrentWorker.CollectData(DA, Params);
+ CurrentWorker.GetData(DA, Params);
// Create the task
var tokenSource = new CancellationTokenSource();
- CurrentRun = new Task(() => CurrentWorker.DoWork(tokenSource.Token, ReportProgress, ReportError, Done), tokenSource.Token);
+ CurrentWorker.CancellationToken = tokenSource.Token;
+ CurrentWorker.Id = DA.Iteration.ToString();
+ Task CurrentRun;
+ if (TaskCreationOptions != null)
+ {
+ CurrentRun = new Task(() => CurrentWorker.DoWork(ReportProgress, ReportError, Done), tokenSource.Token, (TaskCreationOptions)TaskCreationOptions);
+ }
+ else
+ {
+ CurrentRun = new Task(() => CurrentWorker.DoWork(ReportProgress, ReportError, Done), tokenSource.Token);
+ }
// Add cancelation source to our bag
- TokenSources.Add(tokenSource);
- CurrentRun.Start();
+ CancelationSources.Add(tokenSource);
+
+ // Add the worker to our list
+ Workers.Add(CurrentWorker);
+
+ Tasks.Add(CurrentRun);
+
return;
}
- foreach (var (message, type) in Errors)
+ if (SetData)
{
- AddRuntimeMessage(type, message);
+ if (Workers.Count > 0)
+ Workers[--State].SetData(DA);
+
+ if (State == 0)
+ {
+ foreach (var (message, type) in Errors)
+ {
+ AddRuntimeMessage(type, message);
+ }
+
+ CancelationSources.Clear();
+ Workers.Clear();
+ Errors.Clear();
+ Tasks.Clear();
+
+ SetData = false;
+
+ Message = "Done";
+ OnDisplayExpired(true);
+ }
}
-
- OnDisplayExpired(true);
- CurrentWorker.SetData(DA);
-
- Message = "Done";
-
- Errors.Clear();
- State = 0;
}
}
}
diff --git a/GrasshopperAsyncComponent/Base/IAsyncComponentWorker.cs b/GrasshopperAsyncComponent/Base/IAsyncComponentWorker.cs
index 8c5ac07..b0943c0 100644
--- a/GrasshopperAsyncComponent/Base/IAsyncComponentWorker.cs
+++ b/GrasshopperAsyncComponent/Base/IAsyncComponentWorker.cs
@@ -8,37 +8,49 @@ using System.Threading.Tasks;
namespace GrasshopperAsyncComponent
{
- // TODO: Would an an abstract class be better here?
- public interface IAsyncComponentWorker
+
+ ///
+ /// A class that holds the actual compute logic and encapsulates the state it needs. Every needs to have one.
+ ///
+ public abstract class WorkerInstance
{
+ ///
+ /// This token is set by the parent .
+ ///
+ public CancellationToken CancellationToken { get; set; }
///
- /// This function should return a duplicate instance of your class. Make sure any state is duplicated (or not) properly.
+ /// This is set by the parent . You can set it yourself, but it's not really worth it.
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// This is a "factory" method. It should return a fresh instance of this class, but with all the necessary state that you might have passed on directly from your component.
///
///
- IAsyncComponentWorker GetNewInstance();
+ public abstract WorkerInstance Duplicate();
///
- /// Here you can safely set the data of your component, just like you would normally. Important: do not call this method directly! When you are ready, call the provided "Done" action from the DoWork function.
+ /// This method is where the actual calculation/computation/heavy lifting should be done.
+ /// Make sure you always check as frequently as you can if is cancelled. For an example, see the .
+ ///
+ /// Call this to report progress up to the parent component.
+ /// Call this to report errors up to the parent component.
+ /// Call this when everything is done. It will tell the parent component that you're ready to .
+ public abstract void DoWork(Action ReportProgress, Action ReportError, Action Done);
+
+ ///
+ /// Write your data setting logic here. Do not call this function directly from this class. It will be invoked by the parent after you've called `Done` in the function.
///
///
- void SetData(IGH_DataAccess DA);
+ public abstract void SetData(IGH_DataAccess DA);
///
- /// Here you can safely collect the data from your component, just like you would normally. Important: do not call this method directly. It will be invoked by the parent component.
+ /// Write your data collection logic here. Do not call this method directly. It will be invoked by the parent .
///
- /// The magic Data Access class.
- /// The parameters list, in case you need it.
- void CollectData(IGH_DataAccess DA, GH_ComponentParamServer Params);
-
- ///
- /// This where the computation happens. Make sure to check and return if the token is cancelled!
- ///
- /// The cancellation token.
- /// Call this action to report progress. It will be displayed in the component's message bar.
- /// Call this to report a warning or an error.
- /// When you are done computing, call this function to have the parent component invoke the SetData function.
- void DoWork(CancellationToken token, Action ReportProgress, Action ReportError, Action Done);
-
+ ///
+ ///
+ public abstract void GetData(IGH_DataAccess DA, GH_ComponentParamServer Params);
}
+
}
diff --git a/GrasshopperAsyncComponent/GrasshopperAsyncComponent.csproj b/GrasshopperAsyncComponent/GrasshopperAsyncComponent.csproj
index a981acc..1e44e20 100644
--- a/GrasshopperAsyncComponent/GrasshopperAsyncComponent.csproj
+++ b/GrasshopperAsyncComponent/GrasshopperAsyncComponent.csproj
@@ -63,7 +63,8 @@
-
+
+
diff --git a/GrasshopperAsyncComponent/SampleImplementations/SampleAsyncComponent.cs b/GrasshopperAsyncComponent/SampleImplementations/SampleAsyncComponent.cs
deleted file mode 100644
index ec4ab0a..0000000
--- a/GrasshopperAsyncComponent/SampleImplementations/SampleAsyncComponent.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using Grasshopper.Kernel;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace GrasshopperAsyncComponent.SampleImplementations
-{
- public class SampleAsyncComponent : GH_AsyncComponent
- {
- public override Guid ComponentGuid { get => new Guid("DF2B93E2-052D-4BE4-BC62-90DC1F169BF6"); }
-
- protected override System.Drawing.Bitmap Icon { get => null; }
-
- public override GH_Exposure Exposure => GH_Exposure.primary;
-
- public SampleAsyncComponent() : base("Sample Async Component", "ASYNC", "Meaningless labour.", "Samples", "Async")
- {
- Worker = new SampleAsyncComponentWorker();
- }
-
- protected override void RegisterInputParams(GH_InputParamManager pManager)
- {
- pManager.AddIntegerParameter("Max iterations", "M", "How many useless cycles should we spin. Minimum 10, maximum 1000.", GH_ParamAccess.item);
- }
-
- protected override void RegisterOutputParams(GH_OutputParamManager pManager)
- {
- pManager.AddTextParameter("Output", "O", "Will just say hello world after spinning.", GH_ParamAccess.item);
- }
- }
-
- public class SampleAsyncComponentWorker : IAsyncComponentWorker
- {
- int MaxIterations { get; set; } = 100;
-
- public void CollectData(IGH_DataAccess DA, GH_ComponentParamServer Params)
- {
- int _maxIterations = 100;
- DA.GetData(0, ref _maxIterations);
- if (_maxIterations > 1000) MaxIterations = 1000;
- if (_maxIterations < 10) MaxIterations = 10;
-
- MaxIterations = _maxIterations;
- }
-
- public void DoWork(CancellationToken token, Action ReportProgress, Action ReportError, Action Done)
- {
- if (token.IsCancellationRequested) return;
-
- for (int i = 0; i <= MaxIterations; i++)
- {
- var sw = new SpinWait();
- for (int j = 0; j <= 100; j++)
- sw.SpinOnce();
-
- ReportProgress(((double)(i + 1) / (double)MaxIterations).ToString("0.00%"));
-
- if (token.IsCancellationRequested) return;
- }
-
- Done();
- }
-
- public IAsyncComponentWorker GetNewInstance()
- {
- return new SampleAsyncComponentWorker();
- }
-
- public void SetData(IGH_DataAccess DA)
- {
- DA.SetData(0, "Hello world. I'm done spinning.");
- }
- }
-}
diff --git a/GrasshopperAsyncComponent/SampleImplementations/Sample_PrimeCalculatorAsyncComponent.cs b/GrasshopperAsyncComponent/SampleImplementations/Sample_PrimeCalculatorAsyncComponent.cs
new file mode 100644
index 0000000..acadebc
--- /dev/null
+++ b/GrasshopperAsyncComponent/SampleImplementations/Sample_PrimeCalculatorAsyncComponent.cs
@@ -0,0 +1,103 @@
+using Grasshopper.Kernel;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GrasshopperAsyncComponent.SampleImplementations
+{
+ public class Sample_PrimeCalculatorAsyncComponent : GH_AsyncComponent
+ {
+ public override Guid ComponentGuid { get => new Guid("22C612B0-2C57-47CE-B9FE-E10621F18933"); }
+
+ protected override System.Drawing.Bitmap Icon { get => null; }
+
+ public override GH_Exposure Exposure => GH_Exposure.primary;
+
+ public Sample_PrimeCalculatorAsyncComponent() : base("Sample Async Component", "PRIME", "Calculates the nth prime number.", "Samples", "Async")
+ {
+ BaseWorker = new PrimeCalculatorWorker();
+ }
+
+ protected override void RegisterInputParams(GH_InputParamManager pManager)
+ {
+ pManager.AddIntegerParameter("N", "N", "Which n-th prime number. Minimum 1, maximum one million. Take care, it can burn your CPU.", GH_ParamAccess.item);
+ }
+
+ protected override void RegisterOutputParams(GH_OutputParamManager pManager)
+ {
+ pManager.AddTextParameter("Output", "O", "The n-th prime number.", GH_ParamAccess.item);
+ }
+ }
+
+ public class PrimeCalculatorWorker : WorkerInstance
+ {
+ int TheNthPrime { get; set; } = 100;
+ long ThePrime { get; set; } = -1;
+
+ public override void DoWork(Action ReportProgress, Action ReportError, Action Done)
+ {
+ // 👉 Checking for cancellation!
+ if (CancellationToken.IsCancellationRequested) return;
+
+ int count = 0;
+ long a = 2;
+
+ // Thanks Steak Overflow (TM) https://stackoverflow.com/a/13001749/
+ while (count < TheNthPrime)
+ {
+ // 👉 Checking for cancellation!
+ if (CancellationToken.IsCancellationRequested) return;
+
+ long b = 2;
+ int prime = 1;// to check if found a prime
+ while (b * b <= a)
+ {
+ // 👉 Checking for cancellation!
+ if (CancellationToken.IsCancellationRequested) return;
+
+ if (a % b == 0)
+ {
+ prime = 0;
+ break;
+ }
+ b++;
+ }
+
+ ReportProgress(((double)(count) / TheNthPrime).ToString("0.00%"));
+
+ if (prime > 0)
+ {
+ count++;
+ }
+ a++;
+ }
+
+ ThePrime = --a;
+ Done();
+ }
+
+ public override WorkerInstance Duplicate() => new PrimeCalculatorWorker();
+
+ public override void GetData(IGH_DataAccess DA, GH_ComponentParamServer Params)
+ {
+ int _nthPrime = 100;
+ DA.GetData(0, ref _nthPrime);
+ if (_nthPrime > 1000000) _nthPrime = 1000000;
+ if (_nthPrime < 1) _nthPrime = 1;
+
+ TheNthPrime = _nthPrime;
+ }
+
+ public override void SetData(IGH_DataAccess DA)
+ {
+ // 👉 Checking for cancellation!
+ if (CancellationToken.IsCancellationRequested) return;
+
+ DA.SetData(0, $"Worker {Id}: {TheNthPrime}th prime is: {ThePrime}");
+ }
+ }
+
+}
diff --git a/GrasshopperAsyncComponent/SampleImplementations/Sample_UslessCyclesComponent.cs b/GrasshopperAsyncComponent/SampleImplementations/Sample_UslessCyclesComponent.cs
new file mode 100644
index 0000000..dcb6041
--- /dev/null
+++ b/GrasshopperAsyncComponent/SampleImplementations/Sample_UslessCyclesComponent.cs
@@ -0,0 +1,80 @@
+using Grasshopper.Kernel;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GrasshopperAsyncComponent.SampleImplementations
+{
+ public class Sample_UselessCyclesAsyncComponent : GH_AsyncComponent
+ {
+ public override Guid ComponentGuid { get => new Guid("DF2B93E2-052D-4BE4-BC62-90DC1F169BF6"); }
+
+ protected override System.Drawing.Bitmap Icon { get => null; }
+
+ public override GH_Exposure Exposure => GH_Exposure.primary;
+
+ public Sample_UselessCyclesAsyncComponent() : base("Sample Async Component", "CYCLOMATRON-X", "Spins uselessly.", "Samples", "Async")
+ {
+ BaseWorker = new UselessCyclesWorker();
+ }
+
+ protected override void RegisterInputParams(GH_InputParamManager pManager)
+ {
+ pManager.AddIntegerParameter("N", "N", "Number of spins.", GH_ParamAccess.item);
+ }
+
+ protected override void RegisterOutputParams(GH_OutputParamManager pManager)
+ {
+ pManager.AddTextParameter("Output", "O", "Nothing really interesting.", GH_ParamAccess.item);
+ }
+ }
+
+ public class UselessCyclesWorker : WorkerInstance
+ {
+ int MaxIterations { get; set; } = 100;
+
+ public override void DoWork(Action ReportProgress, Action ReportError, Action Done)
+ {
+ // Checking for cancellation
+ if (CancellationToken.IsCancellationRequested) return;
+
+ for (int i = 0; i <= MaxIterations; i++)
+ {
+ var sw = new SpinWait();
+ for (int j = 0; j <= 100; j++)
+ sw.SpinOnce();
+
+ ReportProgress(((double)(i + 1) / (double)MaxIterations).ToString("0.00%"));
+
+ // Checking for cancellation
+ if (CancellationToken.IsCancellationRequested) return;
+ }
+
+ Done();
+ }
+
+ public override WorkerInstance Duplicate() => new UselessCyclesWorker();
+
+ public override void GetData(IGH_DataAccess DA, GH_ComponentParamServer Params)
+ {
+ if (CancellationToken.IsCancellationRequested) return;
+
+ int _maxIterations = 100;
+ DA.GetData(0, ref _maxIterations);
+ if (_maxIterations > 1000) _maxIterations = 1000;
+ if (_maxIterations < 10) _maxIterations = 10;
+
+ MaxIterations = _maxIterations;
+ }
+
+ public override void SetData(IGH_DataAccess DA)
+ {
+ if (CancellationToken.IsCancellationRequested) return;
+ DA.SetData(0, $"Hello world. Worker {Id} has spun for {MaxIterations} iterations.");
+ }
+ }
+
+}