Suppose we have 2 worker functions:
void Step1(); // Maybe long.
void Step2(); // Might be short clean up of step 1.
I often see:
Task.Run(() => Step1()).ContinueWith(t => Step2());
Which creates 2 tasks which run in series. When:
Task.Run(() => { Step1(); Step2(); });
Which creates a single task which runs the 2 functions in series, might appear to be a SIMPLER choice.
Are there common sense guidelines that can be applied to determine when a continuation is actaully required over the simpler approach?
The above examples don't have exception handling - to what extend does exception handling affect those guidelines?
Are there common sense guidelines that can be applied to determine when a continuation is actaully required over the simpler approach?
ContinueWith
provides you with the ability to invoke Step2
only on certain conditions via the TaskContinutationOptions
, such as OnlyOnCanceled
OnlyOnFaulted
, OnlyOnRanToCompletion
, and more. That way, you can compose a workflow which is suitable for each case.
You could also do this with a single Task.Run
and a try-catch
, but that would probably be more for you to maintain.
Personally, I attempt to avoid using ContinueWith
as I find async-await
to be less verbose, and more synchronous like. I would rather await
inside a try-catch
.
Usually, use the least amount of continuations that will do. They clutter the code and cost performance.
One reason to do this is exception behavior. The continuation will run even if the first task failed. Here, there is no error behavior as far as I can tell. This does not seem to be an issue in this particular piece of code. You would somehow need to process the exception from t
.
Often, people are thinking "I have a pipeline!" and are decomposing the pipeline into steps. That's natural to think. But the pipeline steps do not necessarily need to manifest in the form of continuations. They can just be sequenced method calls.
There's two main reasons I see:
The ContinueWith
approach allows you to easily compose many different tasks, and use helper methods to build "continuation trees". Changing this to imperative calls limits this - it's still possible, but tasks are much more composable than imperative code.
In the ContinueWith
case, Step2
always runs, even if Step1
throws. This could be emulated with a try
clause, of course, but it's a bit trickier. Most importantly, it doesn't compose, and it doesn't scale well - if you find you have to run multiple steps, each with their own error handling, you're going to struggle a lot with try-catch
es. Of course, Task
s aren't the only solution to this, nor are they necessarily the best - an error monad will allow you to compose interdependent operations easily as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With