Skip to content

Commit a50809b

Browse files
authored
refactor!: Major code cleanup for clearer state (#27)
* Quick pass * no locks * github actions * publish via github actions * pack * Parent non-optional * Generics are fun * second pass * Group workers, CTS, and Task in a class for simpler nullability * pollysharp * reverted cancellation changes * reset state * Update samples * changes * still working * the fix * private workers * comments * comments 2 * comments 3 * public worker count
1 parent 442b06d commit a50809b

5 files changed

Lines changed: 178 additions & 133 deletions

File tree

GrasshopperAsyncComponent/GH_AsyncComponent.cs

Lines changed: 109 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,76 @@
1-
using System.Collections.Concurrent;
1+
using System.Collections.Concurrent;
22
using System.Diagnostics;
33
using Grasshopper.Kernel;
44
using Timer = System.Timers.Timer;
55

66
namespace GrasshopperAsyncComponent;
77

8+
internal sealed class Worker<T> : IDisposable
9+
where T : GH_Component
10+
{
11+
public required WorkerInstance<T> Instance { get; init; }
12+
13+
public required Task Task { get; init; }
14+
15+
public required CancellationTokenSource CancellationSource { get; init; }
16+
17+
public void Cancel() => CancellationSource.Cancel();
18+
19+
public void Dispose()
20+
{
21+
if (Task.IsCompleted)
22+
{
23+
Task.Dispose();
24+
}
25+
CancellationSource.Dispose();
26+
}
27+
}
28+
829
/// <summary>
930
/// Inherit your component from this class to make all the async goodness available.
1031
/// </summary>
11-
public abstract class GH_AsyncComponent : GH_Component, IDisposable
32+
public abstract class GH_AsyncComponent<T> : GH_Component, IDisposable
33+
where T : GH_Component
1234
{
1335
//List<(string, GH_RuntimeMessageLevel)> Errors;
1436

1537
private readonly Action<string, double> _reportProgress;
1638

17-
public ConcurrentDictionary<string, double> ProgressReports { get; protected set; }
18-
19-
private readonly Action _done;
39+
public ConcurrentDictionary<string, double> ProgressReports { get; }
2040

2141
private readonly Timer _displayProgressTimer;
2242

23-
private int _state;
24-
25-
private int _setData;
43+
/// <summary>
44+
/// a counter, used to count up the number of workers that have completed,
45+
/// until _setData is set true, when it starts to count down the workers as their data is set.
46+
/// </summary>
47+
private volatile int _state;
2648

27-
public List<WorkerInstance> Workers { get; protected set; }
49+
/// <summary>
50+
/// functionally, a boolean, 1 or 0;
51+
/// it will be set to 1 once all workers are ready for SetData to be called on them, then set back to 0.
52+
/// </summary>
53+
private volatile int _setData;
2854

29-
private readonly List<Task> _tasks;
55+
private readonly List<Worker<T>> _workers;
3056

31-
public List<CancellationTokenSource> CancellationSources { get; }
57+
public int WorkerCount => _workers.Count;
3258

3359
/// <summary>
3460
/// Set this property inside the constructor of your derived component.
3561
/// </summary>
36-
public WorkerInstance? BaseWorker { get; set; }
62+
public WorkerInstance<T>? BaseWorker { get; set; }
3763

3864
/// <summary>
3965
/// Optional: if you have opinions on how the default system task scheduler should treat your workers, set it here.
4066
/// </summary>
41-
public TaskCreationOptions? TaskCreationOptions { get; set; }
67+
public TaskCreationOptions TaskCreationOptions { get; set; } = TaskCreationOptions.None;
4268

4369
protected GH_AsyncComponent(string name, string nickname, string description, string category, string subCategory)
4470
: base(name, nickname, description, category, subCategory)
4571
{
46-
Workers = new List<WorkerInstance>();
47-
CancellationSources = new List<CancellationTokenSource>();
48-
_tasks = new List<Task>();
72+
_workers = new List<Worker<T>>();
73+
4974
ProgressReports = new ConcurrentDictionary<string, double>();
5075

5176
_displayProgressTimer = new Timer(333) { AutoReset = false };
@@ -59,36 +84,36 @@ protected GH_AsyncComponent(string name, string nickname, string description, st
5984
_displayProgressTimer.Start();
6085
}
6186
};
87+
}
6288

63-
_done = () =>
89+
private void Done()
90+
{
91+
Interlocked.Increment(ref _state);
92+
if (_state == _workers.Count && _setData == 0)
6493
{
65-
Interlocked.Increment(ref _state);
66-
if (_state == Workers.Count && _setData == 0)
67-
{
68-
Interlocked.Exchange(ref _setData, 1);
69-
70-
// We need to reverse the workers list to set the outputs in the same order as the inputs.
71-
Workers.Reverse();
72-
73-
Rhino.RhinoApp.InvokeOnUiThread(
74-
(Action)
75-
delegate
76-
{
77-
ExpireSolution(true);
78-
}
79-
);
80-
}
81-
};
94+
Interlocked.Exchange(ref _setData, 1);
95+
96+
// We need to reverse the workers list to set the outputs in the same order as the inputs.
97+
_workers.Reverse();
98+
99+
Rhino.RhinoApp.InvokeOnUiThread(
100+
(Action)
101+
delegate
102+
{
103+
ExpireSolution(true);
104+
}
105+
);
106+
}
82107
}
83108

84109
public virtual void DisplayProgress(object sender, System.Timers.ElapsedEventArgs e)
85110
{
86-
if (Workers.Count == 0 || ProgressReports.Values.Count == 0)
111+
if (_workers.Count == 0 || ProgressReports.Values.Count == 0)
87112
{
88113
return;
89114
}
90115

91-
if (Workers.Count == 1)
116+
if (_workers.Count == 1)
92117
{
93118
Message = ProgressReports.Values.Last().ToString("0.00%");
94119
}
@@ -100,7 +125,7 @@ public virtual void DisplayProgress(object sender, System.Timers.ElapsedEventArg
100125
total += kvp.Value;
101126
}
102127

103-
Message = (total / Workers.Count).ToString("0.00%");
128+
Message = (total / _workers.Count).ToString("0.00%");
104129
}
105130

106131
Rhino.RhinoApp.InvokeOnUiThread(
@@ -121,29 +146,24 @@ protected override void BeforeSolveInstance()
121146

122147
Debug.WriteLine("Killing");
123148

124-
foreach (var source in CancellationSources)
149+
foreach (var currentWorker in _workers)
125150
{
126-
source.Cancel();
151+
currentWorker.Cancel();
127152
}
128153

129-
CancellationSources.Clear();
130-
Workers.Clear();
131-
ProgressReports.Clear();
132-
_tasks.Clear();
133-
134-
Interlocked.Exchange(ref _state, 0);
154+
ResetState();
135155
}
136156

137157
protected override void AfterSolveInstance()
138158
{
139-
System.Diagnostics.Debug.WriteLine("After solve instance was called " + _state + " ? " + Workers.Count);
159+
Debug.WriteLine("After solve instance was called " + _state + " ? " + _workers.Count);
140160
// We need to start all the tasks as close as possible to each other.
141-
if (_state == 0 && _tasks.Count > 0 && _setData == 0)
161+
if (_state == 0 && _workers.Count > 0 && _setData == 0)
142162
{
143-
System.Diagnostics.Debug.WriteLine("After solve INVOKATION");
144-
foreach (var task in _tasks)
163+
Debug.WriteLine("After solve INVOCATION");
164+
foreach (var worker in _workers)
145165
{
146-
task.Start();
166+
worker.Task.Start();
147167
}
148168
}
149169
}
@@ -170,31 +190,30 @@ protected override void SolveInstance(IGH_DataAccess da)
170190

171191
// Add cancellation source to our bag
172192
var tokenSource = new CancellationTokenSource();
173-
CancellationSources.Add(tokenSource);
174193

175194
var currentWorker = BaseWorker.Duplicate($"Worker-{da.Iteration}", tokenSource.Token);
176-
if (currentWorker == null)
177-
{
178-
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Could not get a worker instance.");
179-
return;
180-
}
181195

182196
// Let the worker collect data.
183197
currentWorker.GetData(da, Params);
184198

185-
var currentRun =
186-
TaskCreationOptions != null
187-
? new Task(
188-
() => currentWorker.DoWork(_reportProgress, _done),
189-
tokenSource.Token,
190-
(TaskCreationOptions)TaskCreationOptions
191-
)
192-
: new Task(() => currentWorker.DoWork(_reportProgress, _done), tokenSource.Token);
199+
var currentRun = new Task<Task>(
200+
async () =>
201+
{
202+
await currentWorker.DoWork(_reportProgress, Done).ConfigureAwait(true);
203+
},
204+
tokenSource.Token,
205+
TaskCreationOptions
206+
);
193207

194208
// Add the worker to our list
195-
Workers.Add(currentWorker);
196-
197-
_tasks.Add(currentRun);
209+
_workers.Add(
210+
new()
211+
{
212+
Instance = currentWorker,
213+
Task = currentRun,
214+
CancellationSource = tokenSource,
215+
}
216+
);
198217

199218
return;
200219
}
@@ -204,42 +223,45 @@ protected override void SolveInstance(IGH_DataAccess da)
204223
return;
205224
}
206225

207-
if (Workers.Count > 0)
226+
if (_workers.Count > 0)
208227
{
209228
Interlocked.Decrement(ref _state);
210-
Workers[_state].SetData(da);
229+
_workers[_state].Instance.SetData(da);
211230
}
212231

213232
if (_state != 0)
214233
{
215234
return;
216235
}
217236

218-
CancellationSources.Clear();
219-
Workers.Clear();
220-
ProgressReports.Clear();
221-
_tasks.Clear();
237+
foreach (var worker in _workers)
238+
{
239+
worker?.Dispose();
240+
}
222241

223-
Interlocked.Exchange(ref _setData, 0);
242+
ResetState();
224243

225244
Message = "Done";
226245
OnDisplayExpired(true);
227246
}
228247

229-
public void RequestCancellation()
248+
private void ResetState()
230249
{
231-
foreach (var source in CancellationSources)
232-
{
233-
source.Cancel();
234-
}
235-
236-
CancellationSources.Clear();
237-
Workers.Clear();
250+
_workers.Clear();
238251
ProgressReports.Clear();
239-
_tasks.Clear();
240252

241253
Interlocked.Exchange(ref _state, 0);
242254
Interlocked.Exchange(ref _setData, 0);
255+
}
256+
257+
public void RequestCancellation()
258+
{
259+
foreach (var worker in _workers)
260+
{
261+
worker.Cancel();
262+
}
263+
264+
ResetState();
243265
Message = "Cancelled";
244266
OnDisplayExpired(true);
245267
}
@@ -260,6 +282,10 @@ protected virtual void Dispose(bool disposing)
260282

261283
if (disposing)
262284
{
285+
foreach (var worker in _workers)
286+
{
287+
worker?.Dispose();
288+
}
263289
_displayProgressTimer?.Dispose();
264290
}
265291
}

GrasshopperAsyncComponent/GrasshopperAsyncComponent.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
<ItemGroup Label="Package References">
2323
<PackageReference Include="Grasshopper" Version="7.4.21078.1001" IncludeAssets="compile;build" PrivateAssets="all" />
24+
<PackageReference Include="PolySharp" Version="1.14.1" />
2425
</ItemGroup>
2526

2627
<ItemGroup>

GrasshopperAsyncComponent/WorkerInstance.cs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
using Grasshopper.Kernel;
1+
using Grasshopper.Kernel;
22

33
namespace GrasshopperAsyncComponent;
44

55
/// <summary>
6-
/// A class that holds the actual compute logic and encapsulates the state it needs. Every <see cref="GH_AsyncComponent"/> needs to have one.
6+
/// A class that holds the actual compute logic and encapsulates the state it needs. Every <see cref="GH_AsyncComponent{T}"/> needs to have one.
77
/// </summary>
8-
public abstract class WorkerInstance(GH_Component? parent, string id, CancellationToken cancellationToken)
8+
public abstract class WorkerInstance<T>(T parent, string id, CancellationToken cancellationToken)
9+
where T : GH_Component
910
{
1011
/// <summary>
1112
/// The parent component. Useful for passing state back to the host component.
1213
/// </summary>
13-
public GH_Component? Parent { get; set; } = parent;
14+
public T Parent { get; set; } = parent;
1415

1516
public CancellationToken CancellationToken { get; } = cancellationToken;
1617

@@ -22,24 +23,29 @@ public abstract class WorkerInstance(GH_Component? parent, string id, Cancellati
2223
/// <param name="id">A Unique id for the new duplicate</param>
2324
/// <param name="cancellationToken">A cancellationToken to be passed to the new duplicate</param>
2425
/// <returns></returns>
25-
public abstract WorkerInstance? Duplicate(string id, CancellationToken cancellationToken);
26+
public abstract WorkerInstance<T> Duplicate(string id, CancellationToken cancellationToken);
2627

2728
/// <summary>
2829
/// This method is where the actual calculation/computation/heavy lifting should be done.
29-
/// <b>Make sure you always check as frequently as you can if <see cref="WorkerInstance.CancellationToken"/> is cancelled. For an example, see the PrimeCalculatorWorker example.</b>
30+
/// <b>Make sure you always check as frequently as you can if <see cref="WorkerInstance{T}.CancellationToken"/> is cancelled. For an example, see the PrimeCalculatorWorker example.</b>
3031
/// </summary>
32+
/// <remarks>
33+
/// If you don't need <see langword="async"/> function, then you can simply return <see cref="Task.CompletedTask"/>
34+
/// Either way, you should be sure to handle exceptions in this function. Otherwise, they will be Unobserved!
35+
/// You can call <paramref name="done"/> on <see cref="Exception"/>s, but avoid calling it when cancellation is has been observed.
36+
/// </remarks>
3137
/// <param name="reportProgress">Call this to report progress up to the parent component.</param>
3238
/// <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>
33-
public abstract void DoWork(Action<string, double> reportProgress, Action done);
39+
public abstract Task DoWork(Action<string, double> reportProgress, Action done);
3440

3541
/// <summary>
36-
/// 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"/> function.</b>
42+
/// 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{T}"/> after you've called `Done` in the <see cref="DoWork"/> function.</b>
3743
/// </summary>
3844
/// <param name="da"></param>
3945
public abstract void SetData(IGH_DataAccess da);
4046

4147
/// <summary>
42-
/// 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>
48+
/// Write your data collection logic here. <b>Do not call this method directly. It will be invoked by the parent <see cref="GH_AsyncComponent{T}"/>.</b>
4349
/// </summary>
4450
/// <param name="da"></param>
4551
/// <param name="parameters"></param>

0 commit comments

Comments
 (0)