BCS
BCS

Reputation: 78545

Are constructors thread safe in C++ and/or C++11?

Derived from this question and related to this question:

If I construct an object in one thread and then convey a reference/pointer to it to another thread, is it thread un-safe for that other thread to access the object without explicit locking/memory-barriers?

// thread 1
Obj obj;

anyLeagalTransferDevice.Send(&obj);
while(1); // never let obj go out of scope

// thread 2
anyLeagalTransferDevice.Get()->SomeFn();

Alternatively: is there any legal way to convey data between threads that doesn't enforce memory ordering with regards to everything else the thread has touched? From a hardware standpoint I don't see any reason it shouldn't be possible.

To clarify; the question is with regards to cache coherency, memory ordering and whatnot. Can Thread 2 get and use the pointer before Thread 2's view of memory includes the writes involved in constructing obj? To miss-quote Alexandrescu(?) "Could a malicious CPU designer and compiler writer collude to build a standard conforming system that make that break?"

Upvotes: 22

Views: 4820

Answers (5)

AdvSphere
AdvSphere

Reputation: 986

Read this question until now... Still will post my comments:

Static Local Variable

There is a reliable way to construct objects when you are in a multi-thread environment, that is using a static local variable (static local variable-CppCoreGuidelines),

From the above reference: "This is one of the most effective solutions to problems related to initialization order. In a multi-threaded environment the initialization of the static object does not introduce a race condition (unless you carelessly access a shared object from within its constructor)."

Also note from the reference, if the destruction of X involves an operation that needs to be synchronized you can create the object on the heap and synchronize when to call the destructor.

Below is an example I wrote to show the Construct On First Use Idiom, which is basically what the reference talks about.

#include <iostream>
#include <thread>
#include <vector>

class ThreadConstruct
{
public:
    ThreadConstruct(int a, float b) : _a{a}, _b{b}
    {
        std::cout << "ThreadConstruct construct start" << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(2));
        std::cout << "ThreadConstruct construct end" << std::endl;
    }

    void get()
    {
        std::cout << _a << " " << _b << std::endl;
    }

private:
    int _a;
    float _b;
};


struct Factory
{
    template<class T, typename ...ARGS>
    static T& get(ARGS... args)
    {
        //thread safe object instantiation
        static T instance(std::forward<ARGS>(args)...);
        return instance;
    }
};

//thread pool
class Threads
{
public:
    Threads() 
    {
        for (size_t num_threads = 0; num_threads < 5; ++num_threads) {
            thread_pool.emplace_back(&Threads::run, this);
        }
    }

    void run()
    {
        //thread safe constructor call
        ThreadConstruct& thread_construct = Factory::get<ThreadConstruct>(5, 10.1);
        thread_construct.get();
    }

    ~Threads() 
    {
        for(auto& x : thread_pool) {
            if(x.joinable()) {
                x.join();
            }
        }
    }

private:
    std::vector<std::thread> thread_pool;
};


int main()
{
    Threads thread;

    return 0;
}

Output:

ThreadConstruct construct start
ThreadConstruct construct end
5 10.1
5 10.1
5 10.1
5 10.1
5 10.1

Upvotes: 0

nosid
nosid

Reputation: 50044

Reasoning about thread-safety can be difficult, and I am no expert on the C++11 memory model. Fortunately, however, your example is very simple. I rewrite the example, because the constructor is irrelevant.

Simplified Example

Question: Is the following code correct? Or can the execution result in undefined behavior?

// Legal transfer of pointer to int without data race.
// The receive function blocks until send is called.
void send(int*);
int* receive();

// --- thread A ---
/* A1 */   int* pointer = receive();
/* A2 */   int answer = *pointer;

// --- thread B ---
           int answer;
/* B1 */   answer = 42;
/* B2 */   send(&answer);
           // wait forever

Answer: There may be a data race on the memory location of answer, and thus the execution results in undefined behavior. See below for details.


Implementation of Data Transfer

Of course, the answer depends on the possible and legal implementations of the functions send and receive. I use the following data-race-free implementation. Note that only a single atomic variable is used, and all memory operations use std::memory_order_relaxed. Basically this means, that these functions do not restrict memory re-orderings.

std::atomic<int*> transfer{nullptr};

void send(int* pointer) {
    transfer.store(pointer, std::memory_order_relaxed);
}

int* receive() {
    while (transfer.load(std::memory_order_relaxed) == nullptr) { }
    return transfer.load(std::memory_order_relaxed);
}

Order of Memory Operations

On multicore systems, a thread can see memory changes in a different order as what other threads see. In addition, both compilers and CPUs may reorder memory operations within a single thread for efficiency - and they do this all the time. Atomic operations with std::memory_order_relaxed do not participate in any synchronization and do not impose any ordering.

In the above example, the compiler is allowed to reorder the operations of thread B, and execute B2 before B1, because the reordering has no effect on the thread itself.

// --- valid execution of operations in thread B ---
           int answer;
/* B2 */   send(&answer);
/* B1 */   answer = 42;
           // wait forever

Data Race

C++11 defines a data race as follows (N3290 C++11 Draft): "The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior." And the term happens before is defined earlier in the same document.

In the above example, B1 and A2 are conflicting and non-atomic operations, and neither happens before the other. This is obvious, because I have shown in the previous section, that both can happen at the same time.

That's the only thing that matters in C++11. In contrast, the Java Memory Model also tries to define the behavior if there are data races, and it took them almost a decade to come up with a reasonable specification. C++11 didn't make the same mistake.


Further Information

I'm a bit surprised that these basics are not well known. The definitive source of information is the section Multi-threaded executions and data races in the C++11 standard. However, the specification is difficult to understand.

A good starting point are Hans Boehm's talks - e.g. available as online videos:

There are also a lot of other good resources, I have mentioned elsewhere, e.g.:

Upvotes: 17

Kevin Anderson
Kevin Anderson

Reputation: 7010

As others have alluded to, the only way in which a constructor is not thread-safe is if something somehow gets a pointer or reference to it before the constructor is finished, and the only way that would occur is if the constructor itself has code that registers the this pointer to some type of container which is shared across threads.

Now in your specific example, Branko Dimitrijevic gave a good complete explanation how your case is fine. But in the general case, I'd say to not use something until the constructor is finished, though I don't think there's anything "special" that doesn't happen until the constructor is finished. By the time it enters the (last) constructor in an inheritance chain, the object is pretty much fully "good to go" with all of its member variables being initialized, etc. So no worse than any other critical section work, but another thread would need to know about it first, and the only way that happens is if you're sharing this in the constructor itself somehow. So only do that as the "last thing" if you are.

Upvotes: 2

Branko Dimitrijevic
Branko Dimitrijevic

Reputation: 52107

There is no parallel access to the same data, so there is no problem:

  • Thread 1 starts execution of Obj::Obj().
  • Thread 1 finishes execution of Obj::Obj().
  • Thread 1 passes reference to the memory occupied by obj to thread 2.
  • Thread 1 never does anything else with that memory (soon after, it falls into infinite loop).
  • Thread 2 picks-up the reference to memory occupied by obj.
  • Thread 2 presumably does something with it, undisturbed by thread 1 which is still infinitely looping.

The only potential problem is if Send didn't acts as a memory barrier, but then it wouldn't really be a "legal transfer device".

Upvotes: 3

DRVic
DRVic

Reputation: 2481

It is only safe (sort of) if you wrote both threads, and know the first thread is not accessing it while the second thread is. For example, if the thread constructing it never accesses it after passing the reference/pointer, you would be OK. Otherwise it is thread unsafe. You could change that by making all methods that access data members (read or write) lock memory.

Upvotes: 1

Related Questions