Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using (not abusing) ContinueWith

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?

like image 671
Ricibob Avatar asked Aug 19 '15 09:08

Ricibob


Video Answer


3 Answers

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.

like image 173
Yuval Itzchakov Avatar answered Sep 24 '22 16:09

Yuval Itzchakov


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.

like image 40
usr Avatar answered Sep 22 '22 16:09

usr


There's two main reasons I see:

  1. Composition

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.

  1. Error handling

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-catches. Of course, Tasks 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.

like image 42
Luaan Avatar answered Sep 23 '22 16:09

Luaan