@@ -8,6 +8,9 @@ using Timer = System.Timers.Timer;
|
||||
|
||||
namespace GrasshopperAsyncComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Inherit your component from this class to make all the async goodness available.
|
||||
/// </summary>
|
||||
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<CancellationTokenSource> TokenSources = new ConcurrentBag<CancellationTokenSource>();
|
||||
|
||||
Action<string> ReportProgress;
|
||||
|
||||
Action<string, GH_RuntimeMessageLevel> ReportError;
|
||||
@@ -32,9 +27,27 @@ namespace GrasshopperAsyncComponent
|
||||
|
||||
Action Done;
|
||||
|
||||
Timer DisplayProgressTimer;
|
||||
|
||||
int State = 0;
|
||||
|
||||
Timer DisplayProgressTimer;
|
||||
bool SetData = false;
|
||||
|
||||
List<WorkerInstance> Workers;
|
||||
|
||||
List<Task> Tasks;
|
||||
|
||||
List<CancellationTokenSource> CancelationSources;
|
||||
|
||||
/// <summary>
|
||||
/// Set this property inside the constructor of your derived component.
|
||||
/// </summary>
|
||||
public WorkerInstance BaseWorker { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: if you have opinions on how the default system task scheduler should treat your workers, set it here.
|
||||
/// </summary>
|
||||
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<WorkerInstance>();
|
||||
CancelationSources = new List<CancellationTokenSource>();
|
||||
Tasks = new List<Task>();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,37 +8,49 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace GrasshopperAsyncComponent
|
||||
{
|
||||
// TODO: Would an an abstract class be better here?
|
||||
public interface IAsyncComponentWorker
|
||||
|
||||
/// <summary>
|
||||
/// A class that holds the actual compute logic and encapsulates the state it needs. Every <see cref="GH_AsyncComponent"/> needs to have one.
|
||||
/// </summary>
|
||||
public abstract class WorkerInstance
|
||||
{
|
||||
/// <summary>
|
||||
/// This token is set by the parent <see cref="GH_AsyncComponent"/>.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="GH_AsyncComponent"/>. You can set it yourself, but it's not really worth it.
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAsyncComponentWorker GetNewInstance();
|
||||
public abstract WorkerInstance Duplicate();
|
||||
|
||||
/// <summary>
|
||||
/// Here you can safely set the data of your component, just like you would normally. <b>Important: do not call this method directly! When you are ready, call the provided "Done" action from the DoWork function.</b>
|
||||
/// This method is where the actual calculation/computation/heavy lifting should be done.
|
||||
/// <b>Make sure you always check as frequently as you can if <see cref="WorkerInstance.CancellationToken"/> is cancelled. For an example, see the <see cref="GrasshopperAsyncComponent.SampleImplementations.PrimeCalculatorWorker"/>.</b>
|
||||
/// </summary>
|
||||
/// <param name="ReportProgress">Call this to report progress up to the parent component.</param>
|
||||
/// <param name="ReportError">Call this to report errors up to the parent component.</param>
|
||||
/// <param name="Done">Call this when everything is <b>done</b>. It will tell the parent component that you're ready to <see cref="SetData(IGH_DataAccess)"/>.</param>
|
||||
public abstract void DoWork(Action<string> ReportProgress, Action<string, GH_RuntimeMessageLevel> ReportError, Action Done);
|
||||
|
||||
/// <summary>
|
||||
/// Write your data setting logic here. <b>Do not call this function directly from this class. It will be invoked by the parent <see cref="GH_AsyncComponent"/> after you've called `Done` in the <see cref="DoWork(Action{string}, Action{string, GH_RuntimeMessageLevel}, Action)"/> function.</b>
|
||||
/// </summary>
|
||||
/// <param name="DA"></param>
|
||||
void SetData(IGH_DataAccess DA);
|
||||
public abstract void SetData(IGH_DataAccess DA);
|
||||
|
||||
/// <summary>
|
||||
/// Here you can safely collect the data from your component, just like you would normally. <b>Important: do not call this method directly. It will be invoked by the parent component.</b>
|
||||
/// Write your data collection logic here. <b>Do not call this method directly. It will be invoked by the parent <see cref="GH_AsyncComponent"/>.</b>
|
||||
/// </summary>
|
||||
/// <param name="DA">The magic Data Access class.</param>
|
||||
/// <param name="Params">The parameters list, in case you need it.</param>
|
||||
void CollectData(IGH_DataAccess DA, GH_ComponentParamServer Params);
|
||||
|
||||
/// <summary>
|
||||
/// This where the computation happens. Make sure to check and return if the token is cancelled!
|
||||
/// </summary>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <param name="ReportProgress">Call this action to report progress. It will be displayed in the component's message bar.</param>
|
||||
/// <param name="ReportError">Call this to report a warning or an error.</param>
|
||||
/// <param name="Done">When you are done computing, call this function to have the parent component invoke the SetData function.</param>
|
||||
void DoWork(CancellationToken token, Action<string> ReportProgress, Action<string, GH_RuntimeMessageLevel> ReportError, Action Done);
|
||||
|
||||
/// <param name="DA"></param>
|
||||
/// <param name="Params"></param>
|
||||
public abstract void GetData(IGH_DataAccess DA, GH_ComponentParamServer Params);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -63,7 +63,8 @@
|
||||
<Compile Include="Info\GrasshopperAsyncComponentInfo.cs" />
|
||||
<Compile Include="Base\IAsyncComponentWorker.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="SampleImplementations\SampleAsyncComponent.cs" />
|
||||
<Compile Include="SampleImplementations\Sample_PrimeCalculatorAsyncComponent.cs" />
|
||||
<Compile Include="SampleImplementations\Sample_UslessCyclesComponent.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -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<string> ReportProgress, Action<string, GH_RuntimeMessageLevel> 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+103
@@ -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<string> ReportProgress, Action<string, GH_RuntimeMessageLevel> 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}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<string> ReportProgress, Action<string, GH_RuntimeMessageLevel> 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.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user