【问题标题】:Reader/Writer Locks in C++C++ 中的读/写锁
【发布时间】:2010-09-19 15:33:39
【问题描述】:

我正在寻找一个好的 C++ 读/写锁。我们有一个不常写者和许多常读者的用例,并希望对此进行优化。最好我想要一个跨平台的解决方案,但是只有一个 Windows 是可以接受的。

【问题讨论】:

    标签: c++ multithreading locking


    【解决方案1】:

    从 C++ 17 (VS2015) 开始可以使用标准:

    #include <shared_mutex>
    
    typedef std::shared_mutex Lock;
    typedef std::unique_lock< Lock >  WriteLock;
    typedef std::shared_lock< Lock >  ReadLock;
    
    Lock myLock;
    
    void ReadFunction()
    {
         ReadLock r_lock(myLock);
         //Do reader stuff
    }
    
    void WriteFunction()
    {
         WriteLock w_lock(myLock);
         //Do writer stuff
    }
    

    对于较旧的编译器版本和标准,您可以使用 boost 创建读写锁:

    #include <boost/thread/locks.hpp>
    #include <boost/thread/shared_mutex.hpp>
    
    typedef boost::shared_mutex Lock;
    typedef boost::unique_lock< Lock >  WriteLock;
    typedef boost::shared_lock< Lock >  ReadLock;
    

    【讨论】:

    • 这看起来仍然会使写入线程饿死,因为有大量的读取线程和一个写入线程。
    【解决方案2】:

    boost::thread 的较新版本具有读/写锁(1.35.0 及更高版本,显然以前的版本无法正常工作)。

    它们的名称为 shared_lockunique_lockupgrade_lock,并在 shared_mutex 上运行。

    【讨论】:

    • 我们在 1.34.1,这可能是升级的原因:)
    • 线程库从 1.34 -> 1.35 进行了大规模的检修,因此在升级时请注意这一点。进行了一些破坏性的 API 更改,但它们不会对大多数人产生太大影响。新版本更符合提议的C++0x库接口,从长远来看是件好事。
    • 我们不会大量使用大部分线程库,但我们确实将它用于我们的锁。我确实深入研究了细节,将 lock_ops 专门用于一个地方,这样我就可以在我的类型中使用 scoped_lock。我同意从长远来看我们想要更标准的实现。
    • 如果您有幸拥有可用的 C++14 或 C++17,您还可以使用 shared_timed_mutex 和匹配的 shared_lock
    【解决方案3】:

    使用标准的预测试、预构建的东西总是好的(例如,另一个答案建议的 Boost),但这并不是很难自己构建的东西。这是从我的一个项目中提取的一个愚蠢的小实现:

    #include <pthread.h>
    
    struct rwlock {
        pthread_mutex_t lock;
        pthread_cond_t read, write;
        unsigned readers, writers, read_waiters, write_waiters;
    };
    
    void reader_lock(struct rwlock *self) {
        pthread_mutex_lock(&self->lock);
        if (self->writers || self->write_waiters) {
            self->read_waiters++;
            do pthread_cond_wait(&self->read, &self->lock);
            while (self->writers || self->write_waiters);
            self->read_waiters--;
        }
        self->readers++;
        pthread_mutex_unlock(&self->lock);
    }
    
    void reader_unlock(struct rwlock *self) {
        pthread_mutex_lock(&self->lock);
        self->readers--;
        if (self->write_waiters)
            pthread_cond_signal(&self->write);
        pthread_mutex_unlock(&self->lock);
    }
    
    void writer_lock(struct rwlock *self) {
        pthread_mutex_lock(&self->lock);
        if (self->readers || self->writers) {
            self->write_waiters++;
            do pthread_cond_wait(&self->write, &self->lock);
            while (self->readers || self->writers);
            self->write_waiters--;
        }
        self->writers = 1;
        pthread_mutex_unlock(&self->lock);
    }
    
    void writer_unlock(struct rwlock *self) {
        pthread_mutex_lock(&self->lock);
        self->writers = 0;
        if (self->write_waiters)
            pthread_cond_signal(&self->write);
        else if (self->read_waiters)
            pthread_cond_broadcast(&self->read);
        pthread_mutex_unlock(&self->lock);
    }
    
    void rwlock_init(struct rwlock *self) {
        self->readers = self->writers = self->read_waiters = self->write_waiters = 0;
        pthread_mutex_init(&self->lock, NULL);
        pthread_cond_init(&self->read, NULL);
        pthread_cond_init(&self->write, NULL);
    }
    

    pthreads 并不是真正的 Windows 原生,但总体思路就在这里。这种实现稍微偏向于作家(一大群作家可以无限期地饿死读者);如果您希望平衡相反,只需修改writer_unlock

    是的,这是 C 而不是 C++。翻译是留给读者的练习。

    编辑

    Greg Rogers 指出 POSIX 标准确实指定了pthread_rwlock_*。如果您没有pthreads,这将无济于事,但它让我想起:Pthreads-w32 应该有效!不要将此代码移植到非pthreads 以供您自己使用,只需在Windows 上使用Pthreads-w32,在其他任何地方使用本机pthreads

    【讨论】:

    • 我很好奇您为什么在 pthread_mutex_t 之上重新实现 pthread_rwlock_(rd/wr)(un)lock 而不仅仅是使用本机 pthread API。
    • 因为我可以/为了好玩。这个项目有点像我的各种锁定设施的试验台。此外,它现在作为演示很有用,对吧?
    • 在 reader_unlock 函数中,您向写入器发出信号,即使当前有读取器正在读取数据。如果 (self->readers == 0 && self->write_waiters) ... 最好添加到您的条件
    • 我试图在这里提出不同的观点。如果还有读者还在阅读,那么向作者发出信号是没有意义的,因为无论如何作者都不会开始工作,只是抓住锁并花费无用的时间。这是一个效率低下的问题,而不是优先事项。
    • 没有意义是什么意思?只要没有人实际持有锁,即使有读者在等待锁,新的写入者也可以愉快地获取锁,然后继续工作。如果你有源源不断的读者,并且不希望偶尔的短篇作家被他们屏蔽,这是更好的平衡。
    【解决方案4】:

    无论您决定使用什么,都将您的工作负载与简单锁进行基准比较,因为在没有争用的情况下,读/写锁往往比简单互斥锁慢 3-40 倍。

    这里是some reference

    【讨论】:

    • 这很有趣,我们肯定在对所有内容进行基准测试。
    • 链接无效
    • 阅读更多内容:C++ Concurrency in Action 2nd。编。安东尼·威廉姆斯,第 3.3.2 章和后来的第 8 章。
    • 简单测试表明,在某些情况下,读写锁定可能真的很慢:coliru.stacked-crooked.com/a/dce6e59b0f01d513
    【解决方案5】:

    编辑:MSDN 杂志链接不再可用。 CodeProject 文章现在可以在https://www.codeproject.com/Articles/32685/Testing-reader-writer-locks 上找到,并且总结得很好。我还发现了一个关于 Compound Synchronisation Objects 的新 MSDN 链接。

    在 MSDN 上有一个关于读写锁的article,其中介绍了它们的一些实现。它还引入了 Slim 读取器/写入器锁,这是一种随 Vista 引入的内核同步原语。还有一个 CodeProject article 用于比较不同的实现(包括 MSDN 文章的那些)。

    【讨论】:

    • 我对超薄 rw 锁的体验是,与基于互斥锁和信号的锁相比,它的速度非常快。
    • 令人讨厌的是,要使用它,您必须编写运行时操作系统检测并使用函数指针或虚拟类来选择锁定策略。忽略 XP 尚不切实际。
    • 是的,你是对的,Zan。只是想提一下,以防有人发现问题并可以使用它。
    • @vividos:您的 MSDN 链接已损坏。
    【解决方案6】:

    C++17 支持 std::shared_mutex 。它在 MSVC++ 2015 和 2017 中受支持。

    【讨论】:

    【解决方案7】:

    英特尔线程构建模块还提供了几个 rw_lock 变体:

    http://www.threadingbuildingblocks.org/

    它们有一个 spin_rw_mutex 用于非常短的争用时间和一个 queueing_rw_mutex 用于较长时间的争用。前者可用于对性能特别敏感的代码。后者在性能上与 Boost.Thread 或直接使用 pthreads 提供的性能更具可比性。但是配置文件以确保哪个是您的访问模式的胜利。

    【讨论】:

    • 呃,并不是说我一定会建议引入所有 TBB 来获得 rw_locks。
    • 不要根据“特别是性能敏感的代码”来选择变体,而应该根据锁的平均持续时间来选择。如果锁只为读取几个整数而持有并在之后释放,那么自旋将是合适的。否则排队是合适的。
    • @rwong 我完全同意。我打算在上一句中传达适当的时间窗口,也许让它听起来比现在更虚无缥缈。 ;)
    【解决方案8】:

    Boost.Thread 自 1.35.0 版以来已经支持读写锁。这样做的好处是该实现非常跨平台,经过同行评审,实际上是a reference implementation for the upcoming C++0x standard

    【讨论】:

    • 是的,几乎 - 但 Boost.Thread 作为 C++0x 线程的基础并不是上述公认答案的一部分。考虑这些附加信息。 :)
    【解决方案9】:

    我可以推荐ACE library,它提供了多种锁定机制并被移植到各种平台。

    根据问题的边界条件,您可能会发现以下类很有用:

    • ACE_RW_Process_Mutex
    • ACE_Write_GuardACE_Read_Guard
    • ACE_Condition

    【讨论】:

    • 我不介意,但我不认为我现在可以将 ACE 库添加到我们的构建中。我过去用过它们,它们很棒。
    • 任何与 ACE 相关的东西都是不好的。这是一个古老的代码库,记录不充分,无法进入。避免不惜一切代价感染您的代码。
    • 我完全同意@shoosh:即使是答案 404s 中提供的链接 :-( 我正在从事的项目(始于 17 世纪后期)使用它,所以我会有别无选择,只能勉强凑合,但对于任何适度新鲜的代码:不惜一切代价远离 ACE。
    • 关于当代 ACE 的主题,另请参阅 thisthis SO 问题。
    【解决方案10】:

    http://www.codeproject.com/KB/threads/ReaderWriterLock.aspx

    这是一个适合大多数任务的优秀且轻量级的实现。

    【讨论】:

      【解决方案11】:

      Glenn Slayde 的 Win32 多读单写同步锁类

      http://www.glennslayden.com/code/win32/reader-writer-lock

      【讨论】:

        【解决方案12】:

        您可以复制 Sun 出色的 ReentrantReadWriteLock。它包括可选的公平性、锁定降级,当然还有重入等功能。

        是的,它是用 Java 编写的,但即使您不懂任何 Java,您也可以轻松阅读并将其转换为 C++。我链接到的文档包含此实现的所有行为属性,因此您可以确保它符合您的要求。

        如果没有别的,那就是指南。

        【讨论】:

        • Java 和 C++ 在风格上是如此不同,这可能不是一个好主意。内置于 C++ 语言的 Java 线程没有。代码(直接翻译)不是异常安全的。
        • 这完全不正确。事实上,Java 的指令重排序规则比大多数 C++ 编译器宽松,这意味着它更安全。您关于“线程在语言中”的说法没有实际意义,因为线程行为被明确定义为标准的“互斥”和“锁定”系统,我曾经使用过的所有 C++ 线程系统也都有,所以翻译确实是简单。
        • 我认为您错过了异常安全的要点。 Java 不习惯性地使用 RAII,C++ 没有finally,因此在存在异常的情况下,直接从 Java 转换为 C++ 是个坏主意。
        猜你喜欢
        • 1970-01-01
        • 2011-01-21
        • 2016-06-05
        • 1970-01-01
        • 1970-01-01
        • 2011-01-25
        • 2011-12-04
        • 1970-01-01
        相关资源
        最近更新 更多