2

It is a follow up question of Shared_ptr and Memory Visibility in c++ and Create object in thread A, use in thread B. Mutex required?.

This question is more about memory visibility rather than data race.

In Java, I have:

ExecutorService executor = Executors.newSingleThreadExecutor();
Integer i = new Integer(5); // no write to i afterwards
executor.submit(() -> {
    System.out.println(i);
});

I don't think this is thread-safe. Because there is no need to put value 5 in the main memory, it could stay in the main thread's CPU cache. Since there is no memory barrier, the executor thread is not guaranteed to see the value 5. To make sure the value is in the main memory, you either use synchronization, or use AtomicInteger, or volatile int.

If you do something similar with shared_ptr in C++, is it safe?

auto sp = std::make_shared<int>(5); // no write to the int afterwards
myExecutor.submit([sp](){
    std::cout << sp;
});

Is the executor thread guaranteed to see the value 5? Note that the shared_ptr is copied to the lambda, not the int.

Here is a more complete example:

Suppose I have a main thread and worker thread. In main thread I've constructed a shared_ptr<Object> and copy the shared_ptr to the worker thread, is this safe to use the copy of the shared_ptr if there is no synchronization in Object class at all (NO write to the object after construction)?

My main puzzle is, the Object is constructed in main thread on the heap, the shared_ptr is copied but not the Object. Will the worker thread definitely have the memory visibility of the Object? Would it be possible that the value of Object is actually in main thread's CPU cache and not in the main memory?

struct WorkingQueue{
    WorkingQueue()=default;

    void push(std::function<void()> task){
        std::lock_guard<std::mutex> lock{mutex};
        queue.push(std::move(task));
    }

    std::optional<std::function<void()>> popIfNotEmpty(){
        std::lock_guard<std::mutex> lock{mutex};
        if(queue.empty()){
            return std::nullopt;
        }
        auto task = queue.front();
        queue.pop();
        return task;
    }

    bool empty(){
        std::lock_guard<std::mutex> lock{mutex};
        return queue.empty();
    }

    mutable std::mutex mutex;
    std::queue<std::function<void()>> queue;
};

int main(){
    WorkingQueue queue;
    std::atomic<bool> stopFlag{false};
    auto f = std::async(std::launch::async, [&queue, &stopFlag](){
        while(!stopFlag || !queue.empty()){
            auto task = queue.popIfNotEmpty();
            if(task){
                (*task)();
            }
        }
    });
    auto sp = std::make_shared<int>(5);
    queue.push([sp](){
        std::cout << *sp;
    });

    stopFlag = true;
    f.get();
}

Is this programmer guaranteed to output 5?

double-beep
  • 5,031
  • 17
  • 33
  • 41
Hui
  • 571
  • 1
  • 3
  • 9
  • C++ standard smart pointer management is thread safe. Missed to hammer this [dupe](https://stackoverflow.com/questions/9127816/stdshared-ptr-thread-safety-explained). – πάντα ῥεῖ Jun 20 '19 at 09:45
  • 1
    @πάνταῥεῖ Despite the smart pointer, the posted scenario is not thread safe. There is nothing unclear about asking whether it is guaranteed to print 5 (the answer is no). – LWimsey Jun 20 '19 at 21:03
  • The shared pointer is captured by value in the lambda. The lambda construction is _sequenced-before_ locking the queue mutex. Unlocking the queue mutex _synchronizes-with_ locking the same mutex in the worker thread. Releasing the mutex is _sequenced-before_ calling the lambda. It all looks properly synchronized to me. – Useless Jun 24 '19 at 10:00
  • @Eric, in the Java example code in the question, I am using Integer instead of AtomicInteger – Hui Jun 26 '19 at 13:27
  • @Hui, sorry, read the code a bit too quickly. Either way, the write to `i` in the submitting thread will be visible when the task executes because there is a happens-before relation between the actions prior to task submission and the actions after the execution begins, according to the section on [memory consistency properties](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility) in the package summary for `java.util.concurrent`. – Eric Jun 26 '19 at 18:16

1 Answers1

1

is this safe to use the copy of the shared_ptr if there is no synchronization in Object class at all

Yes, std::shared_ptr is synchronized so that it's reference count is thread safe. Read/write synchronization of the object it points to, however, is up to you.

Edit after question edited:

Is the executor thread guaranteed to see the value 5?

No, this is exactly the same as passing a raw pointer to your myExecutor thread.

Paul Evans
  • 27,315
  • 3
  • 37
  • 54