I have a composite asynchronous method:
CancellationTokenSource cts;
protected virtual async void Test() {
cts = new CancellationTokenSource();
await Attack(cts.Token);
}
protected virtual async Task Attack(CancellationToken token) {
token.ThrowIfCancellationRequested();
await MoveToTarget(token);
await DoAttackAnimation(token);
await ReturnToBase(token);
}
Now suppose that I want the attack to cancel under certain conditions dictated by this method:
protected virtual void EnsureConditions() {
if (HP <= 0 || IsParalyzed || TargetIsDead) {
cts?.Cancel();
cts?.Dispose();
cts = null;
}
}
With this, my new attack method has looked like this:
protected virtual async Task Attack(CancellationToken token) {
EnsureConditions();
await MoveToTarget(token);
EnsureConditions();
await DoAttackAnimation(token);
EnsureConditions();
await ReturnToBase(token);
}
But it immediately feels like I'm doing something incorrectly:
- Not only do I need to inject the Ensure method inbetween awaits, but I also need to inject it in the body of Move, Animation and Return since the conditions may have changed.
- Lots of boilerplate in the long term.
So after reconsidering, I figured that the "intended" use of CancellationTokenSource is to be cancelled when an event happens, rather than proactively polling for conditions - something like this:
public void OnHPChanged() {
if (HP <= 0) {
// cancel & dispose CTS
}
}
public void OnParalyzed() {
// cancel & dispose CTS
}
public void OnTargerDied() {
// cancel & dispose CTS
}
And now I don't have to do any injections which seems to be the correct approach to me.
But I would still like to know, is there a better way to execute the "polling predicate" style? My ideal scenario would be this:
protected virtual async void Test() {
cts = new CancellationTokenSource();
cts.AttachPredicate(EnsureConditions); // <--- magically cancels CTS as soon as predicate is true
await Attack(cts.Token);
}
I suppose that with better understanding or control of the order in which tasks are run this could be doable (like a task that runs before all other tasks and checks the predicate) but also sounds a bit iffy.
This is correct.
Sure, you could do that. But it would quite literally be polling a predicate.
E.g.:
But no one does that. It's hacky:
WatchAsyncloop itself should ideally be cancellable.Instead, everyone uses the approach you've already discovered: just cancel the CTS immediately when its cancellation trigger occurs.