Thread Stack

I was a little confused on how thread stacks work. Like I get that each thread has its own Stack on creation, but how do passing variables work and stuff?

I ran into this while working on Rust for my ECE459 assignment:

  • In Rust, std::thread::spawn takes a closure that must be 'static (and usually Send)
  • The thread cannot safely hold a reference to a local variable on main’s stack, because main could return (or just move on) while the thread is still running.
  • So captured variables must be owned by the closure (moved in), or they must be references to something that is guaranteed to live long enough (rarely a local stack variable).

What about in C++?

In C++, we can pass references/pointers to stack locals, but it can be undefined behavior.

To illustrate this, consider the follwing example:

{
    int x = 5;
    std::thread t([&]{ use(x); });  // captures by reference
    t.detach();  // thread might run after main continues/returns
}
  • Since the thread t is running in detached mode, there is a risk that x goes out of scope while t uses it => undefined behavior

But how can x go out of scope if the thread stack is created in the same stack as the the variable x?

Isn’t the thread stack terminated when the std::thread object goes out of scope?

  • Not if you use t.detach()

You see, std::thread is just a handle to an OS thread. Destroying the handle doesn’t “kill” the thread.

In C++, the behavior is like this

{
  std::thread t(work);
} // if still joinable here => std::terminate()

If a std::thread object is destroyed while it’s still joinable, the program calls std::terminate().

We do one of these before the std::thread object goes out of scope:

  1. t.join() (waits for the thread to finish)
  2. t.detach() (lets it run independently)

In Rust

In Rust, we want to prevent this dangerous behavior, which is why the compiler just prevents this from ever being allowed.