2

If I have code that abstracts a staged sequence of asynchronous operations by returning a Task representing each stage, how can I ensure that continuations execute in stage order (i.e. the order in which the Tasks are completed)?

Note that this is a different requirement from simply 'not wasting time waiting for slower tasks'. The order needs to be guaranteed without race conditions in the scheduling. This looser requirement could addressed by parts of the answers to the following questions:

  1. Sort Tasks into order of completition
  2. Is there default way to get first task that finished successfully?

I think the logical solution would be to attach the continuations using a custom TaskScheduler (such as one based on a SynchronizationContext). However, I can't find any assurance that the scheduling of continuations is performed synchronously upon task completion.

In code this could be something like

class StagedOperationSource
{
    public TaskCompletionSource Connect = new TaskCompletionSource();
    public TaskCompletionSource Accept = new TaskCompletionSource();
    public TaskCompletionSource Complete = new TaskCompletionSource();
}
class StagedOperation
{
    public Task Connect, Accept, Complete;
    public StagedOperation(StagedOperationSource source)
    {
        Connect = source.Connect.Task;
        Accept = source.Accept.Task;
        Complete = source.Complete.Task;
    }
}
...
private StagedOperation InitiateStagedOperation(int opId)
{
    var source = new StagedOperationSource();
    Task.Run(GetRunnerFromOpId(opId, source));
    return new StagedOperation(source);
}
...
public RunOperations()
{
    for (int i=0; i<3; i++)
    {
        var op = InitiateStagedOperation(i);
        op.Connect.ContinueWith(t => Console.WriteLine("{0}: Connected", i));
        op.Accept.ContinueWith(t => Console.WriteLine("{0}: Accepted", i));
        op.Complete.ContinueWith(t => Console.WriteLine("{0}: Completed", i));
    }
}

which should produce output similar to

0: Connected
1: Connected
0: Accepted
2: Connected
0: Completed
1: Accepted
2: Accepted
2: Completed
1: Completed

Obviously the example is missing details like forwarding exceptions to (or cancelling) later stages if an earlier stage fails, but its just an example.

Community
  • 1
  • 1
SensorSmith
  • 1,129
  • 1
  • 12
  • 25
  • You might want to look in to [TPL Dataflow](https://msdn.microsoft.com/en-us/library/hh228603(v=vs.110).aspx) – Scott Chamberlain Jun 03 '16 at 17:47
  • You could use a semaphore like waitone or use a lock to ensure one task completes before next one starts. – jdweng Jun 03 '16 at 17:52
  • 1
    @jdweng That wouldn't be asynchronous. To do that you'd have to create a thread and semaphore for each iteration of the loop, making the code *much* more expensive than it needs to be. – Servy Jun 03 '16 at 18:02
  • You can't run completely asynchronous and guarantee the completion order. There has to be some synchronization between tasks especially when you are running the same tasks over and over again. You have a state machine and you have to define the rules to get accurate results. – jdweng Jun 03 '16 at 18:11
  • 1
    @jdweng I posted an answer that runs completely asynchronously and guarantees the ordering required 12 minutes before you posted that comment. No synchronization is needed beyond the synchronization that the TPL provides to ensure that a continuation is run after the task that it is a continuation of has finished. – Servy Jun 03 '16 at 18:15
  • @jdweng that is true, but it was part of my premise that the Tasks ARE completed in a logical predictable order. The state machine/locks or thread completing the tasks was out of the context of the question. The question was (intended to be) focused on ensuring that order is reflected into the continuations. – SensorSmith Jun 03 '16 at 18:46
  • I thought delaying/chaining the attachment of the continuations wouldn't give me all the flexibility I needed but actually it does. OK yet another case of failing to grok just how exceptionally cool async/await really is (especially with some ConfigureAwait style foo). Thanks to all who jumped in with it (whether they beat everyone else to it or not). – SensorSmith Jun 03 '16 at 19:32

2 Answers2

3

Just await each stage before going onto the next...

public static async Task ProcessStagedOperation(StagedOperation operation, int i)
{
    await operation.Connect;
    Console.WriteLine("{0}: Connected", i);
    await operation.Accept;
    Console.WriteLine("{0}: Accepted", i);
    await operation.Complete;
    Console.WriteLine("{0}: Completed", i);
}

You can then call that method in your for loop.

Servy
  • 202,030
  • 26
  • 332
  • 449
2

If you use TAP (Task Asynchronous Programming), i.e. async and await, you can make the flow of processing a lot more apparent. In this case I would create a new method to encapsulate the order of operations:

public async Task ProcessStagedOperation(StagedOperation op, int i)
{
    await op.Connect;
    Console.WriteLine("{0}: Connected", i);

    await op.Accept;
    Console.WriteLine("{0}: Accepted", i)

    await op.Complete;
    Console.WriteLine("{0}: Completed", i)
}

Now your processing loop gets simplified a bit:

public async Task RunOperations()
{
    List<Task> pendingOperations = new List<Task>();

    for (int i=0; i<3; i++)
    {
        var op = InitiateStagedOperation(i);
        pendingOperations.Add(ProcessStagedOperation(op, i));
    }

    await Task.WhenAll(pendingOperations); // finish
}

You now have a reference to a task object you can explicitly wait or simply await from another context. (or you can simply ignore it). The way I modified the RunOperations() method allows you to create a large queue of pending tasks but not block while you wait for them all to finish.

Berin Loritsch
  • 11,400
  • 4
  • 30
  • 57
  • Using task based programming just means that you're using tasks. You don't need to use `await` to do that. The OP was *already* using the TPL. – Servy Jun 03 '16 at 18:00
  • And TAP is built on TPL. Your answer was to use `await` as well, so I'm confused as to the need for the comment. – Berin Loritsch Jun 03 '16 at 18:05
  • TAP is just the model of programming based on using tasks, of which the TPL is one implementation. Like I said, a task based programming model requires programming using tasks, it in no way requires using `await`. I'm not saying there's anything wrong with using `await`, just that your statement that "TAP is defined as using `await`" is wrong. You're also suggesting the OP start using a model that *he was already using*. – Servy Jun 03 '16 at 18:07
  • I never _defined_ TAP as using await, but the concepts are _closely_ associated. It'll help the OP's Google Fu to use TAP or async/await. At any rate it seems like we are making a mountain out of a molehill here. – Berin Loritsch Jun 03 '16 at 18:30
  • The for loop was an artifact of making a simplified example, but your Task.WhenAll is a nice touch to extending what was actually provided. – SensorSmith Jun 03 '16 at 18:56