1

Suppose my code is like the following:

struct Foo {
  Foo() : x(10) {}
  int x_;
}

void WillRunInThread(const Foo* f) {
  cout << "f.x_ is: " << f->x_ << endl;
}

struct Baz {
  Baz() : foo_(), t_(&WillRunInThread, &foo_) {}
  Foo foo_;
  std::thread t_;
}

int main(int argc, char** argv) {
  Baz b;
  b.t_.join();
}

Am I guaranteed that WillRunInThread will print 10? I have seen some StackOverflow answers that indicate this is safe. However, I'm not sure that's the case. Specifically, I think the following two possibilities exist both of which could create issues:

  1. Foo.x_ could be stored in a register when t_ is started.
  2. The compiler could re-order some instructions. While C++ guarantees that foo_ will appear to be constructed before t_ that's only within the same thread. The compiler is free to, for example, re-order the initialization of foo_.x_ until after t_.

I see no indication that std::thread's constructor acts as a memory fence of any kind to prevent the above two issues. However, I see code like the above quite frequently which makes me think I'm missing something. Any clarification would be appreciated.

Oliver Dain
  • 9,617
  • 3
  • 35
  • 48

2 Answers2

7

It's a mistake to think in terms of implementation details like "this variable may be stored in a register" or "the compiler may want to reorder these instructions". Stick to what the standard tells you.

There is a synchronization point when a thread starts (C++17 [thread.thread.constr]/6):

The completion of the invocation of the constructor synchronizes with the beginning of the invocation of the copy of f.

"A synchronizes with B" means that all side effects that have been completed before A (in A's thread) will be visible to all code that runs after B (in B's thread). See [intro.races]/9-12.

In particular, if thread 1 starts a thread using the std::thread constructor, and thread 2 is the thread that starts and its initial function is f, then any code inside f, or inside a function called from f, will see all side effects that were caused by thread 1 prior to executing the std::thread constructor.

Since the initialization of foo_.x_ is sequenced before initialization of t_, it follows that the value of 10 will be read by the new thread.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • The "completion of the invocation of the constructor synchronizes with the beginning of the invocation of the copy of f" bit is what I was missing. Thanks. – Oliver Dain Jul 27 '20 at 19:28
2

Your code as written is certainly safe. (ignoring errors: missing ;s, x instead of x_). This is because foo_ is actually copied into the thread constructor. You can see this by adding Foo(const Foo&) = delete;, and it will fail to compile.

https://en.cppreference.com/w/cpp/thread/thread/thread
The arguments to the thread function are moved or copied by value.

Now, to get at the spirit of your question, let's change the thread initialization to

  Baz() : foo_(), t_(&WillRunInThread, std::ref(foo_)) {}

This is still safe, because:

The completion of the invocation of the constructor synchronizes-with (as defined in std::memory_order) the beginning of the invocation of the copy of f on the new thread of execution.

jtbandes
  • 115,675
  • 35
  • 233
  • 266
  • yes, thanks. I missed that the thread constructor acts as a synchronization point. Also missed that `Foo` is being copied in my example. Will fix that. Thanks. – Oliver Dain Jul 27 '20 at 19:29