Overview
Grasshopper for Rhino allows you to develop multi-threaded components by way of the new IGH_TaskCapableComponent
interface. Benchmarks have shown that Grasshopper can run significantly faster when using multi-threaded components. Results may vary, as not all solutions can be computed in parallel.
The Interface
When a component implements the IGH_TaskCapableComponent
interface, Grasshopper will notice and potentially call a full enumeration of SolveInstance
for the component twice in consecutive passes:
- The first pass is for collecting data and starting tasks to compute results
- The second pass is for using the results from the tasks to set outputs.
Example
In this guide, we will convert a standard component into a task capable component. In our example, the initial component code looks like this:
public class FibonacciComponent : GH_Component { ... protected override void SolveInstance(IGH_DataAccess data) { const int max_steps = 46; int steps = 0; data.GetData(0, ref steps); if (steps < 0) { AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Steps must be >= 0."); return; } if (steps > max_steps) // Prevent overflow... { AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Steps must be <= {max_steps}."); return; } int result; if (steps == 0) result = 0; else if (steps == 1) result = 1; else { int x = 0, y = 1, rc = 0; for (int i = 2; i <= steps; i++) { rc = x + y; x = y; y = rc; } result = rc; } data.SetData(0, result); } ... }
Separate Methods
Now you need to separate calculations into separate methods. Independent tasks should not be directly accessing IGH_DataAccess
, as that interface is not thread safe. Thus, the we will want to break the current flow of a component’s SolveInstance
method into three distinct steps:
- Collect input data
- Compute results on given data
- Set output data
To implement Step 2, we will break out the computation code into its own method.
To start, create a public SolveResults
class to hold the data for each SolveInstance
iteration. For this computation, our definition of SolveInstance
is very simple:
public class SolveResults { public int Value { get; set; } }
Create a Compute function that takes the input retrieved from IGH_DataAccess
and returns an instance of SolveResults
.
private static SolveResults ComputeFibonacci(int n) { SolveResults result = new SolveResults(); if (n == 0) result.Value = 0; else if (n == 1) result.Value = 1; else { int x = 0, y = 1, rc = 0; for (int i = 2; i <= n; i++) { rc = x + y; x = y; y = rc; } result.Value = rc; } return result; }
Implement the Interface
Now, we are ready to launch multiple tasks in the component.
Change the component’s inheritance from GH_Component
to GH_TaskCapableComponent<T>
. In this example, modify the component from this:
public class FibonacciComponent : GH_Component
to this:
public class FibonacciComponent : GH_TaskCapableComponent<FibonacciComponent.SolveResults>
Finally, modify SolveInstance
to use tasks:
protected override void SolveInstance(IGH_DataAccess data) { const int max_steps = 46; if (InPreSolve) { // First pass; collect input data int steps = 0; data.GetData(0, ref steps); if (steps < 0) { AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Steps must be >= 0."); return; } if (steps > max_steps) // Prevent overflow... { AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Steps must be <= {max_steps}."); return; } // Queue up the task Task<SolveResults> task = Task.Run(() => ComputeFibonacci(steps), CancelToken); TaskList.Add(task); return; } if (!GetSolveResults(data, out SolveResults result)) { // Compute right here; collect input data int steps = 0; data.GetData(0, ref steps); if (steps < 0) { AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Steps must be >= 0."); return; } if (steps > max_steps) // Prevent overflow... { AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Steps must be <= {max_steps}."); return; } // Compute results on given data result = ComputeFibonacci(steps); } // Set output data if (result != null) { data.SetData(0, result.Value); } }
The full source code for this revision can be seen here.