1

First, some context (pardon the pun). Consider the following two async methods:

public async Task Async1() {
    PreWork();
    await Async2();
    PostWork();
}

public async Task Async2() {
    await Async3();
}

Thanks to the async and await keywords, this creates the illusion of a nice simple call stack:

  • Async1
    • PreWork
    • Async2
      • Async3
    • PostWork

But, if I understand correctly, in the generated code the call to PostWork is tacked on as a continuation to the Async2 task, so at runtime the flow of execution is actually more like this:

  • Async1
    • PreWork
    • Async2
      • Async3
        • PostWork

(and actually it's even more complicated than that, because in reality the use of async and await causes the compiler to generate state machines for each async method, but we might be able to ignore that detail for this question)

Now, my question: Is there any way to flow some sort of context through these auto-generated continuations, such that by the time I hit PostWork, I have accumulated state from Async1 and Async2?

I can get something similar to what I want with tools like AsyncLocal and CallContext.LogicalSetData, but they aren't quite what I need because the contexts are "rolled back" as you work your way back up the async chain. For example, calling Async1 in the following code will print "1,3", not "1,2,3":

private static readonly AsyncLocal<ImmutableQueue<String>> _asyncLocal = new AsyncLocal<ImmutableQueue<String>>();

public async Task Async1() {
    _asyncLocal.Value = ImmutableQueue<String>.Empty.Enqueue("1");
    await Async2();
    _asyncLocal.Value = _asyncLocal.Value.Enqueue("3");
    Console.WriteLine(String.Join(",", _asyncLocal.Value));
}

public async Task Async2() {    
    _asyncLocal.Value = _asyncLocal.Value.Enqueue("2");
    await Async3();
}

I understand why this prints "1,3" (the execution context flows down to Async2 but not back up to Async1) but that isn't what I'm looking for. I really want to accumulate state through the actual execution chain, such that I'm able to print "1,2,3" at the end because that was the actual way in which the methods were executed leading up to the call to Console.WriteLine.

Note that I don't want to blindly accumulate all state across all async work, I only want to accumulate state that is causally related. In this scenario I want to print "1,2,3" because that "2" came from a dependent (awaited) task. If instead the call to Async2 was just a fire-and-forget task then I wouldn't expect to see "2" because its execution would not be in the actual chain leading up to the Console.WriteLine.

Edit: I do not want to solve this problem just passing around parameters and return values because I need a generic solution that will work across a large code base without having to modify every method to pass around this metadata.

ean5533
  • 8,884
  • 3
  • 40
  • 64
  • 5
    Rather than using variables scoped outside of these methods, simply pass the state that the methods need as parameters, and have them return any new state as return values. That's *far* simpler and much more obviously safe and correct. – Servy Jun 04 '15 at 15:20
  • 1
    What are you trying to do? You can pass parameters to each method with the state you want. You don't need any context to pass data from one method to the next. In fact, you are using the context as a kind of global state, which isn't a great idea – Panagiotis Kanavos Jun 04 '15 at 15:21
  • If your context has some business meaning, eg the context of a specific business transaction like a ticket reservation, you should create an explicit class and pass it from one method to the next. – Panagiotis Kanavos Jun 04 '15 at 15:22
  • @Servy I'm looking for a generic way to get this data passed around without every method needing to explicitly deal with it. Imagine that the call stack has many methods, which may or may not have their own parameters and return values, and only some of the methods actually care about this stuff. If a-->b-->c-->d-->e..., and only `a` and `e` care about this stuff, it would be ugly to have `b` `c` and `d` passing around this information just so that its dependents can get to it. – ean5533 Jun 04 '15 at 15:25
  • Perhaps it would help to explain that the information I'm trying to pass around is meta-data, not related in any way to the methods themselves. – ean5533 Jun 04 '15 at 15:27
  • 4
    @ean5533 I'd imagine that passing some parameters through two or three methods that only pass it on to other methods is going to be simpler, less confusing, and less error prone, than what you're looking for, *particularly* in an async context, but even outside of an async context. The only other viable solution I see is to ensure that each invocation of these methods in their call chains are on a single instance of an item that has fields that you're using, rather than either having static methods or sharing instances for each of these invocations. – Servy Jun 04 '15 at 15:29
  • 3
    Sure sounds like an [X/Y problem](http://meta.stackexchange.com/a/66378/164548) to me. Note that `async` methods *begin executing synchronously*, so the context for `2` is *exactly the same* as the context for `1` - not just the same context, but the same thread, just one method deeper in the call stack. Writing to the LCC generates a clone, but other than that there's not going to be an easy way to distinguish those. – Stephen Cleary Jun 04 '15 at 17:14
  • You may want to look at using a custom awaiter like [this](http://stackoverflow.com/a/22753055/1768303), but it will probably go against the constraint you've added with your last edit. – noseratio Jun 04 '15 at 17:57

0 Answers0