C++中的读写锁实现

5

我正在尝试使用shared_mutex在C++中实现读写锁。

typedef boost::shared_mutex Lock;
typedef boost::unique_lock< Lock >  WriteLock;
typedef boost::shared_lock< Lock >  ReadLock;

class Test {
    Lock lock;
    WriteLock writeLock;
    ReadLock readLock;

    Test() : writeLock(lock), readLock(lock) {}

    readFn1() {
        readLock.lock();
        /*
             Some Code
        */
        readLock.unlock();
    }

    readFn2() {
        readLock.lock();
        /*
             Some Code
        */
        readLock.unlock();
    }

    writeFn1() {
        writeLock.lock();
        /*
             Some Code
        */
        writeLock.unlock();
    }

    writeFn2() {
        writeLock.lock();
        /*
             Some Code
        */
        writeLock.unlock();
    }
}

这段代码似乎可以正常工作,但我有几个概念问题。

问题1:我看到建议在http://en.cppreference.com/w/cpp/thread/shared_mutex/lock上使用unique_lock和shared_lock,但我不明白为什么,因为shared_mutex已经支持lock和lock_shared方法?

问题2:这段代码是否有可能导致写入饥饿?如果是的话,我该如何避免饥饿?

问题3:还有其他锁定类可以尝试实现读写锁吗?


1
如果你观察到写饥饿(或读饥饿),请提交一个错误报告。存在一些算法可以确保不会发生饥饿情况,这就是为什么这段代码中没有读者/写者优先级的API。请访问https://dev59.com/FWYq5IYBdhLWcg3wpyRE#14307116获取这些算法的链接。这里有一篇论文的链接,其中包含了解释和部分实现:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2406.html#shared_mutex_imp - Howard Hinnant
3个回答

3

问题1:使用互斥锁封装器

建议使用封装器对象而不是直接管理互斥锁,以避免不幸的情况,即您的代码被中断而互斥锁未被释放,导致其永久锁定。

这是RAII的原则。

但是,这仅在您的ReadLock或WriteLock局部于使用它的函数时才有效。

例如:

readFn1() {
    boost::unique_lock< Lock > rl(lock);  
    /*
         Some Code 
         ==> imagine exception is thrown
    */
    rl.unlock();   // this is never reached if exception thrown 
}  // fortunately local object are destroyed automatically in case 
   // an excpetion makes you leave the function prematurely      

如果函数中断,你的代码将无法正常工作,因为你的ReadLock WriteLock对象是Test的成员,而不是函数设置锁的本地变量。

Q2:写入饥饿

还不完全清楚你如何调用读者和写者,但是确实存在风险:

  • 只要读者处于活动状态,写者就会被unique_lock阻塞,等待互斥锁以独占模式可获取。
  • 然而,只要写者在等待,新的读者就可以访问共享锁,导致unique_lock进一步延迟。

如果想避免饥饿,必须确保等待的写者有机会设置其unique_lock。例如,在读者中添加一些代码以检查是否有写者正在等待,然后再设置锁。

Q3 其他锁定类

不太确定你要找什么,但我有印象{{link1:condition_variable}}可能对你有用。但逻辑有点不同。

也许,你可以通过打破常规思维来找到解决方案:也许有一种适合的无锁数据结构,可以通过略微改变方法来促进读者和写者的共存?

2
锁的类型是可以的,但是不要将它们作为成员函数,而是在成员函数内部创建 locktype lock(mymutex)。这种方式即使在异常情况下也会在释放时自动解除锁定。

在每个成员函数内,您可以在堆栈上创建所需锁类型的实例“shared_lock(mylock)”。无需释放它,当它在函数退出时超出范围时,它会被释放。即使在异常情况下也可以使用此方法。希望这更像是正确的英语。今天真的很长 :( - systemcpro

1

问题1:我看到建议在http://en.cppreference.com/w/cpp/thread/shared_mutex/lock上使用unique_lock和shared_lock,但我不明白为什么,因为shared_mutex已经支持了lock和lock_shared方法?

可能是因为unique_lock自c++11以来就存在,但shared_lock是在c++17中引入的。此外,unique_lock可能更高效。这是shared_lock的原始理由[由创建者提供] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2406.html,我会遵循这个。

问题2:这段代码有可能导致写入饥饿吗?如果是,则如何避免饥饿?

是的,绝对可能。如果您执行以下操作:

while (1)
    writeFn1();

你最终可能会得到一个时间线:
T1: writeLock.lock()
T2: writeLock.unlock()

T3: writeLock.lock()
T4: writeLock.unlock()

T5: writeLock.lock()
T6: writeLock.unlock()

...

T2-T1的差异是任意的,取决于正在进行的工作量。但是,T3-T2接近于零。这是另一个线程获取锁的窗口。由于窗口非常小,它可能无法获取锁。

为了解决这个问题,最简单的方法是在T2T3之间插入一个小的休眠(例如nanosleep)。您可以通过将其添加到writeFn1的底部来实现此操作。

其他方法可能涉及创建锁的队列。如果一个线程无法获取锁,它会将自己添加到队列中,当锁被释放时,队列上的第一个线程会获取锁。在Linux内核中,这是针对“排队自旋锁”实现的。

Q3.还有其他锁类我可以尝试实现读写锁吗?

虽然不是类,但您可以使用pthread_mutex_lockpthread_mutex_unlock。这些实现递归锁。您可以添加自己的代码来实现等效于boost::scoped_lock的内容。您的类可以控制语义。

或者,boost 有它自己的锁。

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