C++ 并发中的内存可见性(无数据竞争)

Posted

技术标签:

【中文标题】C++ 并发中的内存可见性(无数据竞争)【英文标题】:Memory visibility in C++ concurrency (no data race) 【发布时间】:2019-06-20 09:42:51 【问题描述】:

这是Shared_ptr and Memory Visibility in c++和Create object in thread A, use in thread B. Mutex required?的后续问题。

这个问题更多的是关于内存可见性而不是数据竞争。

在 Java 中,我有:

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

我认为这不是线程安全的。因为不需要将值 5 放入主内存中,它可以保留在主线程的 CPU 缓存中。由于没有内存屏障,执行线程不能保证看到值 5。为了确保该值在主内存中,您可以使用同步,或使用 AtomicInteger,或 volatile int。

如果你在 C++ 中用 shared_ptr 做类似的事情,它安全吗?

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

是否保证执行线程看到值 5?请注意,shared_ptr 被复制到 lambda,而不是 int。

这是一个更完整的例子:

假设我有一个主线程和一个工作线程。在主线程中,我构造了一个shared_ptr&lt;Object&gt; 并将shared_ptr 复制到工作线程,如果Object 类中根本没有同步,那么使用shared_ptr 的副本是否安全(不写入对象施工后)?

我的主要困惑是,对象是在堆上的主线程中构造的,shared_ptr 被复制但不是对象。工作线程一定会拥有Object的内存可见性吗?会不会 Object 的值实际上是在主线程的 CPU 缓存中而不是在主内存中?

struct WorkingQueue
    WorkingQueue()=default;

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

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

    bool empty()
        std::lock_guard<std::mutex> lockmutex;
        return queue.empty();
    

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

int main()
    WorkingQueue queue;
    std::atomic<bool> stopFlagfalse;
    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();

这个程序员保证输出5吗?

【问题讨论】:

C++ 标准智能指针管理是线程安全的。错过了锤这个dupe。 @πάνταῥεῖ 尽管有智能指针,但发布的场景不是线程安全的。询问是否保证打印 5 没有什么不清楚的(答案是否定的)。 共享指针由 lambda 中的值捕获。 lambda 构造是 sequenced-before 锁定队列互斥体。解锁队列互斥锁 synchronizes-with 在工作线程中锁定同一个互斥锁。释放互斥锁是 sequenced-before 调用 lambda。在我看来,这一切都已正确同步。 @Eric,在问题的 Java 示例代码中,我使用的是 Integer 而不是 AtomicInteger @Hui,对不起,读代码有点太快了。无论哪种方式,在任务执行时,提交线程中对i 的写入将是可见的,因为在任务提交之前的操作和执行开始之后的操作之间存在发生之前的关系,根据@987654324 上的部分@在java.util.concurrent的包摘要中。 【参考方案1】:

如果在 Object 类中根本没有同步,那么使用 shared_ptr 的副本是否安全

是的,std::shared_ptr 是同步的,因此它的引用计数是线程安全的。但是,它指向的对象的读/写同步取决于您。

问题编辑后编辑:

执行器线程是否保证看到值 5?

不,这与将原始指针传递给 myExecutor 线程完全相同。

【讨论】:

安全可以通过将它持有的指针传递给魔鬼而消失。

以上是关于C++ 并发中的内存可见性(无数据竞争)的主要内容,如果未能解决你的问题,请参考以下文章

Java内存模型之可见性问题

voliate怎么保证可见性

Java并发基础Java内存模型解决有序性和可见性

并发与高并发-线程安全性-可见性

并发编程-Java内存模型:解决可见性与有序性问题

并发编程三要素:原子性,有序性,可见性