-1

I tried this bit of code

struct Bar {
    public int Value;
}

async Task doItLater(Action fn) {
    await Task.Delay(100);
    fn();
}
void Main() {
    Bar bar = new Bar { Value = 1 }; //Bar is a struct
    doItLater(() => {
        Console.WriteLine(bar.Value);
    }).Wait();
}

and got the output 1. Now this is confusing to me. My logic is as follows

  • Bar is a struct. And therefore all instances should be stored on the stack
  • When the Task.Delay(100) is hit, that thread of execution is done, and the TPL is requested to execute fn() at a later time.
  • bar is stored on the stack and by the time we access it in the closure, that frame that shouldn't exist.

So then how on earth am I getting an output of 1?

George Mauer
  • 117,483
  • 131
  • 382
  • 612
  • 7
    `And therefore all instances should be stored on the stack` That's an incorrect assumption, and so the conclusions drawn from it are not sound. `bar is stored on the stack` is also a false statement. – Servy Sep 05 '17 at 21:34
  • Use a decompiler, like Redgate Reflector or dotPeek or ILSpy, and take a look at the magic. It really isn't that complex. – Ben Voigt Sep 05 '17 at 21:39
  • `bar` is stored on the heap because it participates in a [closure](https://stackoverflow.com/questions/428617/what-are-closures-in-net) due to the anonymous function that references it. If it didn't store it in a closure, the value `bar` would be removed from memory the moment `Main` exits, which would cause a serious problem for your anonymous function. – John Wu Sep 05 '17 at 21:41
  • @Servy I thought instances of `struct`s were stored on the stack? Is that not always true? Is that never true? – George Mauer Sep 05 '17 at 21:41
  • `When the Task.Delay(100) is hit, that thread of execution is done` No, it just returns to its caller. Unless the method is at the top of the call stack the thread won't be done executing. – Servy Sep 05 '17 at 21:42
  • @GeorgeMauer I specifically said that that's not true, so no, it's not true. – Servy Sep 05 '17 at 21:42
  • Relevant: [The Stack Is An Implementation Detail](https://blogs.msdn.microsoft.com/ericlippert/2009/04/27/the-stack-is-an-implementation-detail-part-one/) – Blorgbeard Sep 05 '17 at 21:44
  • @Servy what *part* of it is not true though? That [structs are stored on the stack](https://stackoverflow.com/questions/815354/why-are-structs-stored-on-the-stack-while-classes-get-stored-on-the-heap-net) seems to be true. So maybe you're saying that they're not *always* stored on the stack? Ok, great, so when are they not? – George Mauer Sep 05 '17 at 21:46
  • @GeorgeMauer The statement "structs are stored on the stack" is false. That you're reasoning for why it's true is to a link to an answer that says, "value types don't always go on the stack" doesn't exactly support your position; in fact, it specifically contradicts it. You can read the answer you linked to (and the article *it* links to) for a more detailed explanation. – Servy Sep 05 '17 at 21:48
  • @JohnWu: _"which would cause a serious problem for your anonymous function"_ -- it's useful to note that, while that could be _generally_ true, in this particular example it's not, because the method where `bar` is declared doesn't return until after the anonymous method is called. In this particular example, it would actually be safe for the captured variable to live in the stack (though of course it does not, since the compiler has no feasible way to know that). – Peter Duniho Sep 05 '17 at 21:54
  • If you believe that structs are always stored on the stack then where are the integers in an array of a hundred thousand integers stored? You think there are four hundred thousand bytes allocated off the stack? What if you had three of them? There are only a million bytes on the stack, but you can easily have two million bytes of integer arrays. – Eric Lippert Sep 06 '17 at 22:18
  • 2
    The simple fact is that you're looking at this completely the wrong way. The correct way to think about it is **variables are storage, and storage either is known to live no longer than the method activation, or possibly lives longer than the method activation**. A variable that is known to have a short lifetime can go on the *short lifetime pool*, also known as "the stack". All the other variables have to go on the heap. This is true regardless of whether the variable holds an int or a reference to a string. – Eric Lippert Sep 06 '17 at 22:21

2 Answers2

7

Peter's answer is correct; summing up:

  • Value types do not "go on the stack". Variables go on the stack when their lifetimes are known to be short. The type of the variable is irrelevant; a variable containing an int goes on the heap when the lifetime of the variable is not short. A variable containing a reference to a string goes on the stack if its lifetime is known to be short.

  • await does not "terminate a thread". The whole point of async-await is that it does not require another thread! The point of asynchronous waiting is to keep using the current thread while we wait for an asynchronous operation to complete. Read "There is no thread" if you believe that await has anything to do with threads. It doesn't.

But I want to address your fundamental error regarding the stack as the reification of continuation.

What is continuation? It's just a fancy word for "at this point, what does this program have to do next?"

In normal code -- no awaits, no yields, no lambdas, nothing fancy -- things are pretty straightforward. When you have:

y = 123;
x = f();
g(x);
return y;

the continuation of f is "assign a value to x and pass it to g", and the continuation of g is "run the continuation of whatever method I'm in right now giving it the value of y".

As you know, we can reify continuation in normal programs by using a stack. The stack data structure actually maintains three things:

  • the states of local variables -- this is the activation information
  • the address of the code which is the normal continuation -- the "return address"
  • enough information to compute at runtime the exceptional continuation; that is, "what happens next?" when an exception is thrown.

But this data structure is only a stack because function activations logically form a stack in normal programs. Function Foo calls Bar, and then Bar calls Blah, and then Blah returns to Bar, and Bar returns to Foo.

Now let's throw in a wrinkle:

int y = 123;
Func<int> f = () => y;
return f;

Now the value of local y must be preserved even after the current method returns, because the delegate might be invoked. Therefore y has a lifetime longer than that of the current activation and does not go on the stack.

Or this:

int y = 123;
yield return y;
yield return y;

Now y must be preserved across invocations of MoveNext of the iterator block, so again, y must be a variable that is not on the stack; it has a long lifetime so it must go on the heap. But notice that this is even weirder than the previous case because the activation of the method can be suspended by the yield and resumed by a future MoveNext. Now we have a case where method invocations do not logically form a stack, so the continuation information can no longer go on the stack either.

And await is just the same; again we have a case where a method can be paused, other stuff can happen on the same thread, and then somehow the method gets resumed where it left off.

Await and yield are both examples of a more general feature called coroutines. A normal method can do three things: throw, hang or return. A coroutine can do a fourth thing: suspend, to be resumed later.

Normal methods are just a special case of coroutines; normal methods are coroutines that don't suspend. And since they have this restriction, normal methods get the benefit of being able to use the stack as the reification of their continuation semantics. Coroutines do not. Since the activations of coroutines do not form a stack, the stack is not used for their local variable activation records; nor is it used to store continuation information like return addresses.

You've fallen into the trap of believing that the special case -- routines that do not suspend -- is how the world must be, but that's simply not true. Rather, non-suspending methods are special methods that can be optimized by using a stack to store their continuation information.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
3

Your analysis has missed the mark on more than one point:

•Bar is a struct. And therefore all instances should be stored on the stack

This is not true. A value type object can be stored on the stack, but not all value types are stored on the stack. See Eric Lippert's famous The Stack Is An Implementation Detail article.

In your example, the bar object is in fact not stored on the stack, because it's captured in the closure you pass to doItNow(). The compiler creates a hidden class where the bar object is stored, and this class is allocated on the heap. So the bar object itself is allocated on the heap as well.

•When the Task.Delay(100) is hit, that thread of execution is done, and the TPL is requested to execute fn() at a later time.

Actually, it's when the await is hit. Simply calling Task.Delay() does nothing more than create and start a new Task that will complete in 100 ms. It's not until the await is executed that the doItLater() method returns. Which is not the same as "that thread of execution" being "done". The thread continues (in your case, as far as calling Wait() on the Task object returned by the doItLater() method).

•bar is stored on the stack and by the time we access it in the closure, that frame that shouldn't exist.

Because you call Wait(), even if it were true that the bar object was stored on the stack, that stack frame would still be present when the continuation in doItLater() is executed and the fn() delegate invocation is executed. The Main() method can't return until the Wait() method completes, and that doesn't happen until the doItLater() task is entirely complete (including having invoked the fn delegate that was passed to it).

In other words, even if we ignore the other misconceptions you have, it's not even true that there'd be a problem in this case, because the bar object would still exist regardless.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • Ok, fair enough, I get what you'er saying about `Task.Delay` and the frame being kept open (I tried to do this with `new Thread()` but I'm in linqpad at the moment and it doesn't play well). However point 1 is still confusing as that would indicate that `bar` should actually be a *copy* of that in the containing function, right? Yet I can actually *assign* to `bar` and change its value outside the closure...so then what is going on? JohnWu in the comments implies that a struct will go on the heap if it participates in a closure...is that true? What about when passed somewhere from there? – George Mauer Sep 05 '17 at 21:53
  • Mutable value types are rife with danger, because it's hard to keep track of whether you are dealing with a copy or the original. In your example though, you never copy the value of `bar` anywhere; if you were to modify the `bar` variable, whether by reassigning the variable itself, or by assigning `bar.Value`, then any code using `bar` would see that change. If you copied the value of `bar` to some other variable and then modified _that_ variable, then `bar` would not change. This has nothing to do with stack/heap and everything to do with value type vs reference type semantics. – Peter Duniho Sep 05 '17 at 21:57
  • Right but you said that the closure is compiled to a class. Which means that `bar` must be passed *into* that class. Which should create a copy should it not? Yes that copy would be stored on the heap, but its still a copy. Sorry, I'd reach for the decompiler but like I said, I'm in front of linqpad at the moment, and I'm not proficient enough at reading IL. – George Mauer Sep 05 '17 at 22:02
  • Here's what I'm looking at https://content.screencast.com/users/togakangaroo/folders/Jing/media/32fb4bec-a264-497e-9c3a-dfe8cda499c9/00000553.png – George Mauer Sep 05 '17 at 22:03
  • _"Which means that bar must be passed into that class"_ -- no, it doesn't mean that. It means `bar` _is part of_ that class. Any place you use `bar` in the code, including as a local variable, that's implicitly using the `bar` member of the class. No copying is happening; by capturing the variable, the local variable _is replaced by_ the field in the hidden class. There is no longer any local variable at all, if the variable is captured. – Peter Duniho Sep 05 '17 at 22:05
  • Ok, I can kinda sorta see whats going on here. Need to dig in more https://content.screencast.com/users/togakangaroo/folders/Jing/media/6280bc34-4bba-4c0e-bfa0-dcd0d0322f4b/00000554.png – George Mauer Sep 05 '17 at 22:20
  • You might consider [downloading dotPeek](https://www.jetbrains.com/decompiler/download/). It's a free decompiler that will help you understand the implementation details here a lot better, especially if you find the IL difficult to understand. Make sure you check "Show compiler-generated code" in the Tools/Options dialog (Decompiler tab), and right-click in the Assembly Explorer and choose "Decompiled Sources", otherwise you'll get the "friendly" view of the code where dotPeek has reconstructed your original code rather than showing you the C# equivalent of the actual IL. – Peter Duniho Sep 05 '17 at 23:12