从写入器向读取器发出信号时正确使用 ReentrantReadWriteLock?

Posted

技术标签:

【中文标题】从写入器向读取器发出信号时正确使用 ReentrantReadWriteLock?【英文标题】:Correct usage of ReentrantReadWriteLock while signalling from writer to reader? 【发布时间】:2013-01-11 14:04:36 【问题描述】:

使用模式源于以下原因:

    如果条件不存在,我需要读取线程来等待数据。

    读锁不支持条件,所以条件应取自写锁。

    由于读线程会等待条件,它也应该获取写锁来等待。

我在课堂上有以下锁定义:

private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
protected final Lock readLock = rwl.readLock();
protected final Lock writeLock = rwl.writeLock();
protected final Condition hasData = writeLock.newCondition();

在我的 writer 方法中,我有以下模式:

try 
   writeLock.lock();    

   //...

   if( something_written ) 
      hasData.signalAll();
   


finally 
   writeLock.unlock();

在我的阅读方法中,我有以下模式

try 
   readLock.lock();    

   while( data_absent ) 

      // I need to acquire write lock to wait for condition!
      try 

          // first releasing read lock since we can't acquire write lock otherwise
          // unfortunately this won't release a lock if it was acquired more than once (reentrant)
          readLock.unlock();

          // acquiring write lock to wait it's condition
          writeLock.lock();
          hasData.await(1000, TimeUnit.MILLISECONDS);
      
      finally 

          // releasing write lock back
          writeLock.unlock();

          // reacquiring read lock
          // again see note about reentrancy
          readLock.lock();
      


   

   // reading


finally 
   readLock.unlock();

上面的模式正确吗?

问题是如果reader是可重入的,即多次锁定read,那么释放代码不起作用,reader在获取写锁的那一行挂起。

【问题讨论】:

只是一些观察...您缺少用于嵌套锁定/解锁的 try-finally 构造。按照惯例,您应该在try 块之外使用lock 为什么你已经获得readlock而获得writelock(反之亦然)?这不是使用它们的正确方法。看看API 顺便说一句,如果可能的话,我建议使用 Java 并发 Queues 之一来解决您的问题(这看起来像是典型的生产者/消费者问题)。 @ortang 队列不匹配。 @Dims 似乎 ReentrantReadWriteLock 也不匹配。你没有指定你想做什么...... 【参考方案1】:

这听起来像是经典的生产者/消费者模式,因此我建议您为此目的查看现有数据结构,例如 BlockingQueue 实现。

生产者线程put()队列中的数据,消费者线程take()队列中的数据。

手动同步/锁定应始终是最后的手段。

【讨论】:

我知道BlockingQueue,但它不匹配。我是最后的手段,因为我正在编写自己的具有自定义功能的队列/缓冲区。问题是关于锁,而不是关于预定义的队列类。 这不是答案。您建议绝对使用另一个类,而忽略ReentrantReadWriteLock 类的描述缺点。 @Dims Buddy,冷静下来。根据您问题中的信息,我提供了我认为更好的选择。如果您有其他未在此处包含的限制,那么我不是读心者。 @Dims 下定决心。首先,您因为我提出了 ReentrantReadWriteLock 的替代方案而激怒我,现在您说自己它有缺点。是哪条路?你必须使用 ReentrantReadWriteLock 吗?或者你可以使用其他一些同步机制吗?作为记录: BlockingQueue 满足您在问题中指定的所有标准。它可能不是正确的答案,但是您需要为问题添加更多细节。我在您的代码中看到的唯一错误是您应该在 finally 块中切换 writeLock.unlock() 和 readLock.lock() 的顺序。 @Dims 同样。祝你好运!【参考方案2】:

你的使用模式是错误的:阅读器只能使用读锁;作家也一样。语义是这样的:只要写锁是空闲的,许多读者可以一次获得读锁;只有在没有获取其他锁(读取或写入)时,writer 才能获取写入锁。

在您的编写器代码中,您尝试在仍持有写锁的同时获取读锁;阅读器代码也是如此。

【讨论】:

读锁不支持条件,因此向读线程发出信号的唯一方法是通过写锁【参考方案3】:

这是我在你的情况下会做的:

private final ReentrantReadWriteLock    rwl         = new ReentrantReadWriteLock();
protected final Lock                    readLock    = rwl.readLock();
protected final Lock                    writeLock   = rwl.writeLock();
protected final Condition               hasData     = writeLock.newCondition();


public void write() 

    writeLock.lock();
    try 
        // write data
        // ...
        if (something_written) 
            hasData.signalAll();
        
    
    finally 
        writeLock.unlock();
    


// replace Object by something else
public Object read() throws InterruptedException 

    Object data = tryRead();

    while (data == null) 
        waitForData();
        data = tryRead();
    

    return data;


// replace Object by something else
private Object tryRead() 

    readLock.lock();
    try 
        Object data = null;
        // read data
        // ...
        // if there no data available, return null
        return data;
    
    finally 
        readLock.unlock();
    


private void waitForData() throws InterruptedException 

    writeLock.lock();
    try 
        boolean data_available = // check data
        while (!data_available) 
            hasData.await(1000L, TimeUnit.MILLISECONDS);
            data_available = // check data
        
    
    finally 
        writeLock.unlock();
    

这与典型的 ReadWriteLock 用例的行为相同如果有可供读取的数据。如果不存在数据,则读取器将成为“写入器”(在锁定意义上)并等待直到某些数据可用。循环重复,直到返回一些可用数据(或直到发生中断)。


由于您使用的是 ReadWriteLock,这意味着您期望读取次数比写入次数多得多,因此您选择了一个可以最大限度地减少读取器线程之间争用的锁(readLock)。

方法 waitForData() 将读取器变成“写入器”,因为它们锁定在 writeLock 上,导致所有线程(读取器和写入器)之间的争用增加。但是,由于假定写入比读取少得多,因此不会出现数据在“可用”和“不可用”之间快速切换的情况。换句话说,假设写入很少

如果没有可供读取的数据,那么几乎所有的读取器通常都会在一段时间后阻塞在方法 waitForData() 中,并且会在写入一些新数据时同时收到通知。

如果有一些可用的数据可供读取,那么所有读取器都将简单地读取它,而不会在锁定 readLock 时在线程之间产生任何争用。

【讨论】:

【参考方案4】:

我认为你想要做的是让你的读者等待作家写,然后返回 some 价值。如果没有价值,您希望您的阅读器线程只是等待或休眠。那是对的吗 ? 如果我的理解是正确的,这是一种方法

private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
protected final Lock readLock = rwl.readLock();
protected final Lock writeLock = rwl.writeLock();
protected final Condition hasData = writeLock.newCondition();
private HashMap myData = new HashMap(); //example structure to read and write

private final ReentrantLock dataArrivalLock = new ReentrantLock();
private final Condition dataArrivalSignal = dataArrivalLock.newCondition();

你的作家方法模式:

try 
   writeLock.lock();    

   //...
   myData.put("foo","ffoo"); //write something !!
   if( something_written ) 
      hasData.signalAll();
   


finally 
   writeLock.unlock();

  try 
                //signal other threads that data has been put in
                dataArrivalLock.lock();
                dataArrivalSignal.signalAll();

             finally 
                dataArrivalLock.unlock();
            

您的阅读器方法模式

try 
            boolean gotData = false;
            while (!gotData) 
                try 
                    readLock.lock();
                    if (myData.size() > 0) 
                        gotData = true;
                        //retrieve the data that is written by writer thred!!
                        myData.get("foo");
                    
                 finally 
                    readLock.unlock();
                
                if(!gotData) 
 //sleep the reader thread for x milliseconds. x depends on your application requirement
                  //   Thread.sleep(250);
                    try 
                        //instead of Thread.sleep(), use the dataArrivalLock signal to wakeup
                        dataArrivalLock.lock();
                        dataArrivalSignal.await();
                        //based on how the application works a timed wait might be better !!
                        //dataArrivalSignal.await(250);
                     finally 
                        dataArrivalLock.unlock();
                    
                
            
         catch (Exception e) 
            e.printStackTrace();
         

这样做是强制您的阅读器线程休眠直到写入器线程写入一些数据。

( 代替 Thread.sleep(250),您也可以使用可能 额外的锁 b/w 读写器来做同样的事情)

【讨论】:

即使新数据在休眠时到达,您的线程也会休眠 250。 waitawait 函数的目的是它们可以通过来自另一个线程的信号唤醒。你的代码会浪费时间。 我编辑了我的答案以使用租户锁。我个人认为线程。 sleep() 是更简单的方法(更少的代码)。根据您的应用程序,您可以校准您的睡眠时间。如果数据不断出现,您可以将其设置为低至 50 毫秒 @Zenil 我认为您的 dataArrival 同步逻辑中有一个漏洞。 Reader 线程尝试获取数据,但没有。然后 writer 放入一些数据,并触发 dataArrivalSignal。然后阅读器等待信号。它会等到作者再次写入,留下第一部分数据。 @sharakan 这是真的。但假设是编写器线程将始终运行并写入一些东西,并且有 [多个] 读取器线程在等待数据,这意味着其中一个会唤醒。这可以通过定时等待轻松解决,例如:dataArrivalSignal .await(250)(我用注释修改了我的代码)..Dims 可以决定前进的最佳方式。【参考方案5】:

下面的方法怎么样(cmets在代码中):

public class ReadWrite

    private final Lock readLock;
    private final Lock writeLock;
    private final Condition condition;

    
        ReadWriteLock rwl = new ReentrantReadWriteLock ();
        readLock = rwl.readLock ();
        writeLock = rwl.writeLock ();
        condition = writeLock.newCondition ();
    

    private Object data;

    // If data is there, return it, otherwise, return null
    private Object tryGetData ()
    
        readLock.lock ();
        try
        
            return data; // May return null
        
        finally
        
            readLock.unlock ();
        
    

    // Wait for data if necessary and then return it
    private Object doGetData () throws InterruptedException
    
        writeLock.lock ();
        try
        
            while (data == null)
                condition.await ();

            return data;
        
        finally
        
            writeLock.unlock ();
        
    

    // Used by reader, return value cannot be null, may block
    public Object getData () throws InterruptedException
    
        Object result = tryGetData ();
        return result == null ? doGetData () : result;
    

    // Used by writer, data may be null
    public void setData (Object data)
    
        writeLock.lock ();
        try
        
            Object previousData = this.data;
            this.data = data;
            if (previousData == null && data != null)
                condition.notifyAll ();
        
        finally
        
            writeLock.unlock ();
        
    

【讨论】:

以上是关于从写入器向读取器发出信号时正确使用 ReentrantReadWriteLock?的主要内容,如果未能解决你的问题,请参考以下文章

修改内存映射文件时通知/发出信号

AT24C02写一个数据然后读取一个数据是正确的,但是当写入多个数据时,读出数据就不正确,求指教?

从一个进程向另一个 C++ 发出信号

从 C++ 向 QML 发出信号以读取 Q_PROPERTY 是同步事件吗?

QSerialPort readyRead 信号仅在应用程序被 waitForReadyRead() 阻塞时发出

用MCU通过IIC控制ADV7611BSWZ-RL的寄存器,可是无法读取与无法写入,是啥原因?要怎么解决?