Why does this Task return early? Have I done somet

2019-04-01 19:31发布

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:

  1. There are three phases to the main method.
  2. Phase 1 creates three workers: a, b, and c.
  3. Phase 2 starts an additional worker d upon the completion of tasks b and c, and another worker e upon completion of a and b (the dependency is that these tasks must be created in this order)
  4. 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:

  1. Make the methods synchronous, and register the continuations myself.
  2. Leave the methods marked as async Task, and use the await keyword along with the Task.WhenAll API to schedule the continuations.

2条回答
女痞
2楼-- · 2019-04-01 19:43

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.

    private async Task DoItAsync(string s, int d)
    {
        Console.WriteLine($"starting {s}");
        await Task.Delay(d * 1000);
        Console.WriteLine($"ending {s}");
    }

    private async Task DoItAsync(Task pre1, Task pre2, string s, int d)
    {
        await pre1;
        await pre2;
        await DoItAsync(s, d);
    }

    private async void Form1_Load(object sender, EventArgs e)
    {
        Task atask = DoItAsync("A", 2);
        Task btask = DoItAsync("B", 10);
        Task ctask = DoItAsync("C", 2);
        Task bcdtask = DoItAsync(btask, ctask, "D", 2);
        Task abetask = DoItAsync(btask, atask, "E", 2);
        await bcdtask;
        await abetask;
    }

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.

查看更多
何必那么认真
3楼-- · 2019-04-01 19:43

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.

That's indeed what you missed. In your case .ContinueWith will return a Task<Task> and not just a Task like you expected. That said, this can be easily fixed by using the Unwrap method to convert the nested tasks into a single one:

var worker = new Worker();

var work = worker.StartWork(1, 2000);

var final = work.ContinueWith(_ => worker.StartWork(2, 0)).Unwrap()
    .ContinueWith(ant => worker.StartWork(3, 1500)).Unwrap();

var awaiter = final.ContinueWith(_ => Tuple.Create(_.Id, _.Status));
Console.WriteLine("\"final\" completed with result {0}", awaiter.Result);
Console.WriteLine("Done.");

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) :

static async Task DoWork()
{
    var worker = new Worker();

    await worker.StartWork(1, 2000);
    await worker.StartWork(2, 0);
    await worker.StartWork(3, 1500);
}

static void Main(string[] args)
{
    DoWork().Wait();
    Console.WriteLine("Done.");
}  
查看更多
登录 后发表回答