MVCC实现中的无锁读写器同步

时间:2012-09-12 08:07:58

标签: c++ c++11

我一直在迷恋一些无锁代码的正确性,我真的很感激我能得到的任何输入。我的问题是如何使用acquire& amp;来实现一些所需的线程间同步。在C ++ 11的内存模型中发布语义。在我的问题之前,有些背景......

MVCC中,作者可以安装新版本的对象,而不会影响旧对象版本的读者。但是,如果编写器在具有更高编号的时间戳的读取器已经获得对旧版本的引用时安装新版本的对象,则必须回滚编写器事务&重试。这是为了保持可序列化的快照隔离(因此就像所有成功的事务以时间戳顺序一个接一个地执行)。读者永远不必因写入而重试,但作者可能不得不回滚&如果他们的活动会从具有更高编号的时间戳的读者“拉出地毯”,则重试。要实现此约束,请使用读取时间戳。想法是读者在获取引用之前将对象的读时间戳更新为其自己的时间戳,并且编写器将检查读时间戳以查看是否可以继续该对象的新版本。

假设有两个事务:T1(写入者)和T2(读者)在不同的线程中运行。

T1(作家)这样做:

void
DataStore::update(CachedObject* oldObject, CachedObject* newObject)
{
    .
    .
    .
    COcontainer* container = oldObject->parent();
    tid_t newTID = newObject->revision();
    container->setObject(newObject);
    tid_t* rrp = &container->readRevision;
    tid_t rr = __atomic_load_n(rrp, __ATOMIC_ACQUIRE);
    while (true)
    {
        if (rr > newTID) throw TransactionRetryEx();
        if (__atomic_compare_exchange_n(
            rrp,
            &rr,
            rr,
            false,
            __ATOMIC_RELEASE,
            __ATOMIC_RELAXED)
        {
            break;
        }
    }
}

T2(读者)这样做:

CachedObject*
Transaction::onRead(CachedObject* object)
{
    tid_t tid = Transaction::mine()->tid();
    COcontainer* container = object->parent();
    tid_t* rrp = &container->readRevision;
    tid_t rr = __atomic_load_n(rrp, __ATOMIC_ACQUIRE);
    while (rr < tid)
    {
        if (__atomic_compare_exchange_n(
            rrp,
            &rr,
            tid,
            false,
            __ATOMIC_ACQUIRE,
            __ATOMIC_ACQUIRE))
        {
            break;
        }
    }
    // follow the chain of objects to find the newest one this transaction can use
    object = object->newest();
    // caller can use object now
    return object;
}

这是我担心的情况的简单总结:

     A    B    C
<----*----*----*---->
   timestamp order

A: old object's timestamp
B: new object's timestamp (T1's timestamp)
C: "future" reader's timestamp (T2's timestamp)

* If T2@C reads object@A, T1@B must be rolled back.

如果T1在T2开始之前完全执行(并且T1的效果对T2完全可见),则没有问题。 T2将获取T1安装的对象版本的引用,因为T1的时间戳小于T2,所以可以使用它。 (事务可以“从过去”读取对象,但它不能“窥视未来”)。

如果T2在T1开始之前完全执行(并且T2的效果对T1完全可见),则没有问题。 T1将看到“来自未来”的事务可能已读取旧版本的对象。因此,将回滚T1并创建新事务以重试工作的性能。

当T1和T2同时运行时,麻烦(当然)是保证正确的行为。使用互斥锁消除竞争条件会非常简单,但如果我确信没有别的办法,我只会接受带锁的解决方案。我很确定应该可以通过获取&amp; amp;发布C ++ 11的内存模型。只要我满意代码是正确的,我就会有一些复杂性。我真的希望读者尽可能快地跑,这是MVCC的一个主要卖点。

问题:

1。查看上面(部分)代码,您认为是否存在竞争条件,以便在以下情况下T1无法回滚(通过throw TransactionRetryEx()) T2继续使用旧版本的对象?

2。如果代码有误,请解释原因并请提供正确的指导。

3。即使代码看起来正确,您能看到效率更高吗?

我在DataStore::update()中的推理是,如果对__atomic_compare_exchange_n()的调用成功,则表示“冲突”读者线程尚未更新读取时间戳,因此它还没有遍历对象版本链,以找到刚刚安装的新版本。

我准备买这本书"Transactional Information Systems: Theory, Algorithms, and the Practice of Concurrency Control and Recovery",但我觉得我也打扰你了:DI认为我应该早点买这本书,但我也很确定我什么都不会学到这将使我的大部分工作失效。

我希望我已经提供了足够的信息来回答问题。如果我收到建设性的批评,我会很乐意编辑我的问题,以便更清楚。如果这个问题(或类似的问题)已被提出并且回答,那太好了。

谢谢!

1 个答案:

答案 0 :(得分:1)

这很复杂,我不能对1.和2说什么。但是,关于3,我注意到了一些事情:

当__atomic_compare_exchange_n返回false时,* rrp的当前值被写入rr,因此循环中的__atomic_load()s都是冗余的(在T2中只是将它抛出,在T1中在循环之前执行一次就像在T2)。

作为一般评论,在算法中的其他所有内容完成之前,可能没有必要考虑获取/释放;那么你可以检查“无处不在”的内存屏障有多强。