1

If you heap allocate an object with shared_ptr on thread A , then copy the shared_ptr to another thread without any synchronization. Is the other thread guaranteed to see a fully constructed object?

int main(){
    auto sp = std::make_shared<int>(5);
    auto f=std::async(std::launch::async, [sp](){
    std::cout<<*sp;});
}

Is it guaranteed to print 5?

Evg
  • 25,259
  • 5
  • 41
  • 83
Hui
  • 571
  • 1
  • 3
  • 9
  • 2
    No syncrhonization in a multi-threaded environment = no guarentee... – Omid CompSCI Jun 20 '19 at 04:46
  • 1
    When you say "then copy the memory address to another thread" do you mean while the thread is already running? Or do you mean "then construct a thread with that memory address" (which is what your example shows)? If you call std::async after the value is allocated (like your example) then it will be allocated before std::async is called and it will work - just as if you passed it to any other function. – Jerry Jeremiah Jun 20 '19 at 04:56
  • 1
    I disagree with @OmidCompSCI .. The object in this example is already fully constructed, and the shared pointer is captured by value in the lambda. If this was not guaranteed to work, then all hell would break loose. – paddy Jun 20 '19 at 04:58
  • @paddy, when you say that the object is fully constructed, do you mean that `5` is already in the main memory, not just in the cache? What would ensure that? Suppose that instead of `make_shared`, you allocate memory with `malloc`, put `5` into it and pass the pointer into `async`. Is the new thread guaranteed to see `5` at that location? – Evg Jun 20 '19 at 05:13
  • @Evg , what makes malloc different? Does the atomic reference count in shared_ptr have influence the sequence order of int construction ? – Hui Jun 20 '19 at 06:00
  • @Hui, nothing makes it different, hence my question. With `malloc` there is no such guarantee without proper memory barrier. – Evg Jun 20 '19 at 06:02
  • @Evg, in single thread, shared_ptr construction sequenced before new int, copy of shared_ptr is inter-thread sequenced before shared_ptr construction. Does that mean use of copy must happen before new int? – Hui Jun 20 '19 at 06:40
  • 1
    @Evg The use of `malloc` does not change the outcome. Thread creation is a synchronizing operation (whether or not that requires a memory barrier instruction depends on the hardware platform). The thread in the example would still print `5` – LWimsey Jun 20 '19 at 06:58
  • @LWimsey, this looks like a real answer to the OP's question. Is this true for any thread creation or just for `std::async`? Can I create a new thread without such synchronization, so that I'd have no guarantee to see `5` as an output? – Evg Jun 20 '19 at 07:07
  • 1
    @Evg This applies to any `std::thread` creation. If you create an object `X` and then create a thread while passing `X` to that thread, it is fully synchronized. [this answer](https://stackoverflow.com/questions/46032965/create-object-in-thread-a-use-in-thread-b-mutex-required/46033613#46033613) has references to the relevant part in the C++ standard – LWimsey Jun 20 '19 at 07:15
  • @LWimsey, I understand about objects, but what about pointers? Is this guaranteed to print `5`: `auto* p = malloc(sizeof(int)); auto* i = new (p) int{5}; std::async(std::launch::async, [i]{std::cout << *i;});`? – Evg Jun 20 '19 at 07:40
  • @Evg Yes, the same rules apply.. thread synchronization is about memory, that can be an object or a reference (pointer) to it. – LWimsey Jun 20 '19 at 07:46
  • @LWimsey, let me disturb you again, please. `auto* p = malloc(sizeof(int)); auto* i = new (p) int{5}; *i = 6; std::async(std::launch::async, [i]{std::cout << *i;});` Is this guaranteed to print `6`? – Evg Jun 20 '19 at 08:02
  • 1
    @Evg It's guaranteed because the last write to that memory location is `6`. So that is what is synchronized with the thread. You can also say, the store (`6`) to memory location `i` happens before the load from `i` in the thread. The operations are ordered. – LWimsey Jun 20 '19 at 08:11
  • @Evg But be careful not to write to `i` in the main thread _after_ the call to `async`, because that would introduce a race and trigger undefined behavior – LWimsey Jun 20 '19 at 08:15
  • @LWinsey, thank you for your answer. If the thread is not newly created, what will happen? Assume a worker thread is processing a "queue>". If in my main thread, I have auto sp = std::make_shared(5); push(queue,[sp](){std::cout << *sp;}); // suppose the queue itself have synchronization on push and pop. Would this be safe? Would the worker thread definitely see the value 5? – Hui Jun 20 '19 at 08:25
  • @Hui passing data between 2 threads that are running concurrently requires synchronization.. Wether your example is correct is hard to say, it depends on the actual implementation and how synchronization primitives are used. – LWimsey Jun 20 '19 at 08:38
  • @LWimsey, if the working queue does have lock_guard lock{mutex} on push/pop, and I am copying the shared_ptr to the std::function and 'push' does copy the std::function into the queue. Is there anything else need to be synchronized? – Hui Jun 20 '19 at 08:43
  • @Hui It sounds like mutex only applies to the queue itself, while the `shared_ptr` is accessed outside of the mutex protected area.. That may be a problem. Why don't you put the code together and post it as a new question ? – LWimsey Jun 20 '19 at 08:50
  • @LWimsey, I've put some code here: https://stackoverflow.com/questions/56683185/create-shared-ptrobject-in-thread-a-after-thread-b-is-created-use-copy-of-sha/56683274#56683274 – Hui Jun 20 '19 at 09:56

2 Answers2

1

In your example, the shared_ptr object has been duplicated before std::async returns, hence it still exists in the new thread even if the original shared_ptr is destroyed before the second thread accesses it's copy.

So, the answer is yes. You are passing by value, hence a copy.

Michael Chourdakis
  • 10,345
  • 3
  • 42
  • 78
  • I don’t mean life time. I know there is no life time problem. My question is about memory visibility. Is 5 guaranteed to be in main memory rather than the CPU cache? – Hui Jun 20 '19 at 05:58
  • Why would a user level application care about that? After all, the compiler might internally use a register to store an integer and no use memory at all. Plus, your question doesn't clarify you are interested in the cache. If so, put the asm tag. – Michael Chourdakis Jun 20 '19 at 06:00
  • if it is in the cache, if the other thread is running on another CPU core, it has no access to the value – Hui Jun 20 '19 at 06:02
  • It's impossible that the compiler wouldn't ensure that the copied value is actually copied in memory. If this was true, the whole multithreading thing would blow up. – Michael Chourdakis Jun 20 '19 at 06:07
  • Yes the shared_ptr is copied, but the object it points to is not copied – Hui Jun 20 '19 at 06:16
  • That's the point of a pointer. You have created dynamic memory with make_shared<> (which calls `new`). That memory persists as long as the shared_ptr exists. – Michael Chourdakis Jun 20 '19 at 06:17
  • There is no change to the memory created by the pointer in your example. – Michael Chourdakis Jun 20 '19 at 06:31
0

As someone already pointed out in the comment, your specific scenario above (heap allocated object is accessed via a pointer passed to a newly spawned thread via thread arg) is covered by the answer to this question.

Now for the scenario described in your follow-up question (heap allocated object is accessed in a separate thread via pointer pushed to a queue), the worker thread is guaranteed to see '5' because the queue has to be implemented in a thread safe fashion whereby memory barrier ensures the visibility of the value stored in the heap object (in your case, mutex uses memory barrier behind the scene).

SPD
  • 370
  • 2
  • 9