I'm trying to set up a bunch of workers with some minimal coupling, but I'd like to use C# async
and tasks. Not all of the tasks will be purely asynchronous (some will be completely synchronous). The motivation for doing this is that I want to create some simple methods that execute business logic, and chain them together using the System.Threading.Tasks.Task
API to preserve some notion of ordering. Basically, I want to create a first task, register some continuations, and then wait for the final task to complete.
Here's the simple prototype I built just to see if what I want to do even works:
void Main()
{
var worker = new Worker();
var work = worker.StartWork(1, 2000);
work.ConfigureAwait(false);
var final = work.ContinueWith(_ => worker.StartWork(2, 0))
.ContinueWith(ant => worker.StartWork(3, 1500));
var awaiter = final.ContinueWith(_ => Tuple.Create(_.Id, _.Status));
Console.WriteLine("\"final\" completed with result {0}", awaiter.Result);
Console.WriteLine("Done.");
}
// Define other methods and classes here
class Worker {
internal async Task StartWork(int phase, int delay) {
Console.WriteLine("Entering phase {0} (in Task {1}) with {2} milliseconds timeout.", phase, Task.CurrentId, delay);
if (delay > 0)
{
Console.WriteLine("Do wait for {0} milliseconds.", delay);
await Task.Delay(delay);
}
Console.WriteLine("ending phase {0}", phase);
}
}
The problem seems to be in awaiting waiting on the so-called awaiter
task:
Entering phase 1 (in Task ) with 2000 milliseconds timeout.
Do wait for 2000 milliseconds.
ending phase 1
Entering phase 2 (in Task 769) with 0 milliseconds timeout.
ending phase 2
Entering phase 3 (in Task 770) with 1500 milliseconds timeout.
Do wait for 1500 milliseconds.
"final" completed with result (770, RanToCompletion)
Done.
ending phase 3
Is this just not supported? I thought I understood the Task
API pretty well, but clearly I don't. I think that I can convert this to not use async
or task, and just execute the method completely synchronously, but that seems like a bad method of doing things. The continuations that I want to run are not exactly this (they just accept a CancellationToken
). There's no particular dependency on messages between tasks - I just need to preserve some notion of ordering.
Thank you.
Edit: I incorrectly used the word awaiting
above: I know that accessing Task.Result
is completely synchronous. My apologies.
Edit 2: What I expected to occur was that calling ContinueWith(_ => worker.Start(2, 0))
would return a task to ContinueWith
, and the TPL would internally await the task returned by worker.StartWork
when my user delegate returned a task. Looking at the overload list for ContinueWith
, this was clearly incorrect. Part of what I'm trying to solve is how to wait from the Main
method that schedules the work; I don't want to exit before all continuations have completed.
The motivation I had for using ContinueWith
was that I have requirements similar to the following:
- There are three phases to the main method.
- Phase 1 creates three workers:
a
,b
, andc
. - Phase 2 starts an additional worker
d
upon the completion of tasksb
andc
, and another workere
upon completion ofa
andb
(the dependency is that these tasks must be created in this order) - A process similar to this follows until all the work is complete.
If I understand the feedback in the comments correctly, I have essentially two ways I can do this:
- Make the methods synchronous, and register the continuations myself.
- Leave the methods marked as
async Task
, and use theawait
keyword along with theTask.WhenAll
API to schedule the continuations.
Kevin's answer is good. But based on the comments you seem to have the belief that "continuewith" somehow gives you more power than await when describing the sequencing of a workflow. It does not. Also, the question of how to structure your workflow of your second edit properly without resorting to explicit continuations has not been addressed.
Let's look at your scenario. Your workflow is: we have tasks A, B, C, D and E. Starting D depends on the completion of B and C; starting E depends on the completion of A and B.
Easily done. Remember: await is the sequencing operation on tasks. Any time we wish to say "Y must come after X", we simply put an await X anywhere before Y is started. Conversely, if we do not wish a task to be forced to complete before something, we don't await it.
Here's a tiny little framework to play with; this probably isn't how I would write your real code, but it clearly illustrates the workflow.
Follow along. A, B and C are started. (Remember, THEY ARE ALREADY ASYNCHRONOUS. "await" does not make them asynchronous; await introduces a sequencing point in a workflow.)
Next our two helper tasks are started. The preconditions of D are B and C, so we await B. Let's suppose B is incomplete, so the await returns a task to the caller, representing the workflow of "start d after b and c are done, and wait for d to complete".
Now we start our second helper task. Again, it awaits B. Let's again suppose it is incomplete. We return to the caller.
Now we add the final bit of structure to our workflow. The workflow is not complete until the two helper tasks are complete. The two helper tasks are not complete until D and E are complete, and they will not even start until B and C, in the case of D, or B and A, in the case of E, are completed.
Use this little framework to play around with the timings of when stuff completes and you will see that it is very straightforward to build up dependencies in the workflow by using
await
. That's what it is for. It's called await because it asynchronously waits for a task to complete.That's indeed what you missed. In your case
.ContinueWith
will return aTask<Task>
and not just aTask
like you expected. That said, this can be easily fixed by using theUnwrap
method to convert the nested tasks into a single one:This will give you the desired output. But as other have mentioned, you may want to use async/await instead, as it'll make your code much easier to read and follow (and will protect you from some corner cases of
ContinueWith
) :