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.