C++并发:互斥锁外部的变量可见性

3
我有些困惑,不知道何时变量会被强制写入内存,即使在互斥块之外。很抱歉下面的代码逻辑很复杂,因为我剥离了涉及reader决定某些数据是否过期的逻辑。需要注意的重要事项是,99.9%的情况下,读者将采用fast path,同步必须非常快,这就是为什么我使用原子int32来传递陈旧性和慢路径是否现在必要的原因。
我有以下设置,我“相当”确定没有竞态条件:
#define NUM_READERS 10

BigObject               mSharedObject;
std::atomic_int32_t     mStamp = 1;
std::mutex              mMutex;
std::condition_variable mCondition;
int32_t                 mWaitingReaders = 0;

void reader() {
    for (;;) { // thread loop
        for (;;) { // spin until stamp is acceptible
            int32_t stamp = mStamp.load();
            if (stamp > 0) { // fast path
                if (stampIsAcceptible(stamp) && 
                    mStamp.compare_exchange_weak(stamp, stamp + 1)) {
                    break;
                }
            } else { // slow path
                // tell the loader (writer) that we're halted
                std::unique_lock<mutex> lk(mMutex);
                mWaitingReaders++;
                mCondition.notify_all();
                while (mWaitingReaders != 0) {
                    mCondition.wait(lk);
                } // ###
                lk.unlock();
                // *** THIS IS WHERE loader's CHANGES TO mSharedObject
                // *** MUST BE VISIBLE TO THIS THREAD!
            }
        }
        // stamp acceptible; mSharedObject guaranteed not written to

        mSharedObject.accessAndDoFunStuff();

        mStamp.fetch_sub(1); // part of hidden staleness logic
    }
}

void loader() {
    for (;;) { // thread loop
        // spin until we somehow decide we want to change mSharedObject!
        while (meIsHappySleeping()) {}

        // we want to modify mSharedObject, so set mStamp to 0 and wait
        // for readers to see this and report that they are now waiting
        int32_t oldStamp = mStamp.exchange(0);
        unique_lock<mutex> lk(mMutex);
        while (mWaitingReaders != NUM_READERS) {
            mCondition.wait(lk);
        }
        // all readers are waiting. start writing to mSharedObject
        mSharedObject.loadFromFile("example.foo");
        mStamp.store(oldStamp);
        mWaitingReaders = 0; // report completion
        lk.unlock();
        mCondition.notify_all();
        // *** NOW loader's CHANGES TO mSharedObject
        // *** MUST BE VISIBLE TO THE READER THREADS!
    }
}

void setup() {
    for (int i = 0; i < NUM_READERS; i++) {
        std::thread t(reader); t.detach();
    }
    std::thead t(loader); t.detach();
}

被标记为星号***的部分是我关注的焦点。这是因为尽管我的代码排除了竞态条件(据我所知),但在被loader()写入时,mSharedObject仅由互斥锁保护。由于reader()需要非常快速(如上所述),我不希望对mSharedObject进行只读访问时需要受到互斥锁的保护。
一种“保证”的解决方案是在###行引入一个线程局部变量const BigObject *latestObject,将其设置为&mSharedObject,然后用它进行访问。但这是否是不好的做法?而且真的有必要吗?原子操作/互斥锁释放操作能够保证读者看到更改吗?
谢谢!
1个回答

2

无锁代码,即使只使用原子操作的锁定代码也远非简单。 首先要做的是添加互斥锁并分析在同步中实际损失了多少性能。请注意,当前的mutex实现可能只会进行快速自旋锁,当非争用时这大约是一个原子操作。

如果您想尝试无锁编程,您需要研究原子操作的内存顺序参数。编写者将需要使用..._release与执行..._acquire的读取器同步(或在两个方面都使用顺序一致性)。否则,对任何其他变量的读取/写入可能不可见。


1
是的,我已经花了数小时的时间进行研究,但却一无所获... 我甚至还制作了一个小型的梗图:http://imgur.com/a/Dpx46 目前,我的所有原子操作都是默认的顺序一致性。这对你的答案有影响吗? - bombax
我刚刚注意到另一件事。在读取器的 *** 段之后,它会回到循环,连续访问 mStamp,然后调用 mSharedObject.accessAndDoFunStuff()。但是 mStamp 是由 loader()loadFromFile() 之后进行 seq-con 写入的。这感觉应该足以强制 reader() 看到 mSharedObject 的变化。我对吗?谢谢! - bombax

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接