3

Consider this code (extracted from Simple-Web-Server, but knowledge of the library shouldn't be necessary to answer this question):

HttpServer server;
thread server_thread;

server.config.port = 8080;
server.default_resource["GET"] = [](shared_ptr<HttpServer::Response> response, shared_ptr<HttpServer::Request> request) {
    string content = "Hello world!"
    *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.size() << "\r\n\r\n" << content;
};

server_thread = thread([&server]() {
    server.start();
});

HttpServer::default_resource is a std::unordered_map, which, to my understanding, isn't thread-safe. port is an unsigned short.

Assuming my understanding of C++ memory fences is correct, server, as seen by the new thread, might not be in a valid state as the main thread might not have written the changes to port and default_resource to memory accessible from other threads. As such, server.start() might not work properly.

To fix this, I would have to change the code by adding to atomic_thread_fences:

HttpServer server;
thread server_thread;

server.config.port = 8080;
server.default_resource["GET"] = [](shared_ptr<HttpServer::Response> response, shared_ptr<HttpServer::Request> request) {
    string content = "Hello world!"
    *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.size() << "\r\n\r\n" << content;
};

atomic_thread_fence(memory_order_release);

server_thread = thread([&server]() {
    atomic_thread_fence(memory_order_acquire);
    server.start();
});

Is my understanding correct, and are both the atomic_thread_fences necessary?

Bernard
  • 5,209
  • 1
  • 34
  • 64
  • 3
    All operations performed by the parent thread ordered before the initialisation of a thread must be complete from the perspective of the thread that has just been created. In this case, therefore, the thread fence is not needed as server has been initialised before the call to `std::thread::thread()`. – Thomas Russell Jan 02 '17 at 13:03
  • It seems that you're confusing the actual thread (a concurrency concept) with the thread handler object (a C++ object). – Kerrek SB Jan 02 '17 at 13:08

1 Answers1

8

30.3.1.2 thread constructors

template <class F, class ...Args> 
explicit thread(F&& f, Args&&... args);

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

In other words: when the thread function gets invoked, it is synchronized with everything that happened in the parent thread up until std::thread gets constructed, in the parent thread.

No explicit memory barriers/fences, of this kind, are needed.

T.C.
  • 133,968
  • 17
  • 288
  • 421
Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • Doesn't the OPs code rather work just because `server` is passed as argument and at that point all previous code with side-effects that affected that variable must have been executed? Suppose for example that `server` was not passed as argument, but instead was a global - if I understand things correctly, then that would have caused synchronization issues. The quoted text merely says that the thread constructor must be executed before the thread callback. – Lundin Jan 02 '17 at 14:36
  • A memory fence wouldn't be required even if pthread_create() is used instead of std::thread. The rule of thumb is: creating a thread or waiting for another thread using mutexes or other synchronization primitives issues the required fences implicitly. You don't have to do it yourself. Unfortunately I can't provide a proof link for that. – Joseph Artsimovich Jan 02 '17 at 17:10
  • Would it be the same when joining a thread, i.e. explicit fences are not required after calling thread::join()? – Bernard Jan 02 '17 at 23:05
  • 1
    @Bernard: Yes, joining a thread implicitly issues the necessary fences to ensure memory stores by that thread are visible to the thread performing the join. – Joseph Artsimovich Jan 07 '17 at 02:11