如何在C++11中实现自己的读写锁?

49

我有一组数据结构,需要使用读者/写者锁进行保护。我知道boost::shared_lock,但我想使用std::mutex、std::condition_variable和/或std::atomic来自定义实现,以便更好地理解它的工作原理(并稍后进行调整)。

每个数据结构(可移动,但不可复制)都将继承一个名为Commons的类,该类封装了锁定操作。我希望公共接口看起来像这样:

class Commons {
public:
    void read_lock();
    bool try_read_lock();
    void read_unlock();

    void write_lock();
    bool try_write_lock();
    void write_unlock();
};

...以便它可以被一些人公开继承:

class DataStructure : public Commons {};

我正在编写科学代码,一般可以避免数据竞争;这个锁主要是为了防止我以后可能犯的错误。因此,我的优先级是尽量减少读取开销,这样不会过度干扰正确运行的程序。每个线程可能都会在自己的CPU核心上运行。

请问您能否展示一个(伪代码也可以)读者/作者锁?我现在使用的是预防写入者饥饿的变体。到目前为止,我的主要问题是在检查读取是否安全并实际增加读取计数之间存在的空隙,之后写入锁就知道要等待了。

void Commons::write_lock() {
    write_mutex.lock();
    reading_mode.store(false);
    while(readers.load() > 0) {}
}

void Commons::try_read_lock() {
    if(reading_mode.load()) {
        //if another thread calls write_lock here, bad things can happen
        ++readers; 
        return true;
    } else return false;
}

我对多线程还比较陌生,希望能够深入了解。非常感谢您的帮助!


6
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2406.html#shared_mutex 提供了共享互斥锁/升级互斥锁的参考实现,可作为读写锁使用。 - yohjp
2
这个问题是关于理解如何自己实现它,但如果您可以使用C++14或更高版本,并且更喜欢使用C++标准中现有的读/写锁,请参考这个问题:https://dev59.com/R43da4cB1Zd3GeqPzGOQ(“如何在C++14中实现读/写锁”)。 - Philipp Claßen
https://godbolt.org/g/vVVvGB - WaltK
4个回答

60

这是一个使用互斥锁和条件变量实现的非常简单的读写锁伪代码。互斥锁API应该是不言自明的。假设条件变量有一个成员wait(Mutex&),它原子性地释放互斥锁并等待条件发生。条件满足时可以使用signal()向一个等待者发送信号或使用signal_all()向全部等待者发送信号来唤醒他们。

read_lock() {
  mutex.lock();
  while (writer)
    unlocked.wait(mutex);
  readers++;
  mutex.unlock();
}

read_unlock() {
  mutex.lock();
  readers--;
  if (readers == 0)
    unlocked.signal_all();
  mutex.unlock();
}

write_lock() {
  mutex.lock();
  while (writer || (readers > 0))
    unlocked.wait(mutex);
  writer = true;
  mutex.unlock();
}

write_unlock() {
  mutex.lock();
  writer = false;
  unlocked.signal_all();
  mutex.unlock();
}

然而,该实现有相当多的缺点。

每当锁可用时都会唤醒所有等待者

如果大多数等待者正在等待写入锁定,那么这是浪费的 - 毕竟,大多数等待者将无法获取锁,并恢复等待。简单地使用signal()是不行的,因为您确实希望唤醒所有等待读取锁解锁的人。因此,要解决这个问题,您需要分别为可读性和可写性设置条件变量。

没有公平性。读者会饿死写者

您可以通过跟踪未处理的读/写锁数量来修复它,并在有待处理的写锁时停止获取读锁(尽管然后您将让读者挨饿!),或随机唤醒所有读者或一个写者(假设您使用了分离的条件变量,请参见上面的章节)。

锁没有按照请求顺序分配

为了保证这一点,您需要一个真正的等待队列。例如,您可以为每个等待者创建一个条件变量,并且在释放锁后向队列头部的所有读者或单个写者发信号。

即使是纯读取工作负载也会由于互斥而引起争用

这个很难解决。一种方法是使用原子指令来获取读或写锁(通常是比较和交换)。如果获取失败,因为锁已被占用,则必须回退到互斥信号量。但正确地执行此操作相当困难。此外,仍将存在争用 - 原子指令远非免费,尤其是在具有许多内核的计算机上。

结论

正确实现同步原语是困难的。实现高效且公平的同步原语则更加困难。而且它几乎从不划算。例如,在Linux上的pthread中,包含了一个使用futexes和原子指令组合的读/写锁,因此可能优于您在几天工作中能够想出的任何东西。


2
你提到的最后一个缺点——读者之间的争用,正是我真正想要解决的问题。我最终得到了一种在我运行的每个特定测试案例中都能工作但在锁定的随机序列中失败的实现。在那时,我放弃了并简单地使用了boost::shared_lock。:( - jack

6

点击这个链接查看这个类:

//
// Multi-reader Single-writer concurrency base class for Win32
//
// (c) 1999-2003 by Glenn Slayden (glenn@glennslayden.com)
//
//


#include "windows.h"

class MultiReaderSingleWriter
{
private:
    CRITICAL_SECTION m_csWrite;
    CRITICAL_SECTION m_csReaderCount;
    long m_cReaders;
    HANDLE m_hevReadersCleared;

public:
    MultiReaderSingleWriter()
    {
        m_cReaders = 0;
        InitializeCriticalSection(&m_csWrite);
        InitializeCriticalSection(&m_csReaderCount);
        m_hevReadersCleared = CreateEvent(NULL,TRUE,TRUE,NULL);
    }

    ~MultiReaderSingleWriter()
    {
        WaitForSingleObject(m_hevReadersCleared,INFINITE);
        CloseHandle(m_hevReadersCleared);
        DeleteCriticalSection(&m_csWrite);
        DeleteCriticalSection(&m_csReaderCount);
    }


    void EnterReader(void)
    {
        EnterCriticalSection(&m_csWrite);
        EnterCriticalSection(&m_csReaderCount);
        if (++m_cReaders == 1)
            ResetEvent(m_hevReadersCleared);
        LeaveCriticalSection(&m_csReaderCount);
        LeaveCriticalSection(&m_csWrite);
    }

    void LeaveReader(void)
    {
        EnterCriticalSection(&m_csReaderCount);
        if (--m_cReaders == 0)
            SetEvent(m_hevReadersCleared);
        LeaveCriticalSection(&m_csReaderCount);
    }

    void EnterWriter(void)
    {
        EnterCriticalSection(&m_csWrite);
        WaitForSingleObject(m_hevReadersCleared,INFINITE);
    }

    void LeaveWriter(void)
    {
        LeaveCriticalSection(&m_csWrite);
    }
};

我没有试过,但代码看起来还不错。


谢谢,这看起来很不错。它还包括一个将读者提升为作者的简单方法,这通常是一个很好的功能。 - Lothar
1
尽管这仅适用于Windows,而不是C++11通用。 - Andy Krouwel

1
我相信这就是你所寻找的内容:

我相信这就是你所寻找的内容:

class Commons {
    std::mutex write_m_;
    std::atomic<unsigned int> readers_;

public:
    Commons() : readers_(0) {
    }

    void read_lock() {
        write_m_.lock();
        ++readers_;
        write_m_.unlock();
    }

    bool try_read_lock() {
        if (write_m_.try_lock()) {
            ++readers_;
            write_m_.unlock();
            return true;
        }
        return false;
    }

    // Note: unlock without holding a lock is Undefined Behavior!
    void read_unlock() {
        --readers_;
    }

    // Note: This implementation uses a busy wait to make other functions more efficient.
    //       Consider using try_write_lock instead! and note that the number of readers can be accessed using readers()
    void write_lock() {
        while (readers_) {}
        if (!write_m_.try_lock())
            write_lock();
    }

    bool try_write_lock() {
        if (!readers_)
            return write_m_.try_lock();
        return false;
    }

    // Note: unlock without holding a lock is Undefined Behavior!
    void write_unlock() {
        write_m_.unlock(); 
    }

    int readers() { 
        return readers_; 
    }
};

就记录而言,从C++17开始我们有了std::shared_mutex,详情请见:https://en.cppreference.com/w/cpp/thread/shared_mutex


1
您可以按照维基百科的算法(我编写的)实现读者-写者锁,具体内容请参见这里
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

int g_sharedData = 0;
int g_readersWaiting = 0;
std::mutex mu;
bool g_writerWaiting = false;
std::condition_variable cond;

void reader(int i)
{
    std::unique_lock<std::mutex> lg{mu};
    while(g_writerWaiting)
        cond.wait(lg);
    ++g_readersWaiting;
    // reading
    std::cout << "\n reader #" << i << " is reading data = " << g_sharedData << '\n';
    // end reading
    --g_readersWaiting;
    while(g_readersWaiting > 0)
        cond.wait(lg);
    cond.notify_one();
}

void writer(int i)
{
    std::unique_lock<std::mutex> lg{mu};
    while(g_writerWaiting)
        cond.wait(lg);
    // writing
    std::cout << "\n writer #" << i << " is writing\n";
    g_sharedData += i * 10;
    // end writing
    g_writerWaiting = true;
    while(g_readersWaiting > 0)
        cond.wait(lg);
    g_writerWaiting = false;
    cond.notify_all();
}//lg.unlock()


int main()
{
    std::thread reader1{reader, 1};
    std::thread reader2{reader, 2};
    std::thread reader3{reader, 3};
    std::thread reader4{reader, 4};
    std::thread writer1{writer, 1};
    std::thread writer2{writer, 2};
    std::thread writer3{writer, 3};
    std::thread writer4{reader, 4};

    reader1.join();
    reader2.join(); 
    reader3.join();
    reader4.join();
    writer1.join();
    writer2.join();
    writer3.join();
    writer4.join();

    return(0);
}

你优先考虑读者?通常我们做的是相反的:优先考虑作者,以便数据能尽快更新给读者。此外,读者通常比作者更多。 - Adrian Maire

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