ReentrantLock的Condition如何工作signallAll()?

huangapple go评论79阅读模式
英文:

ReentrantLock Condition how does signallAll() work?

问题

我正在尝试理解signalAll()如何不会破坏关键部分例如

    //这里有一个类试图使用在单独线程中初始化的资源
    
    ReentrantLock lock = new ReentrantLock();
    Condition wait = lock.newCondition();
    
    Resource res = new Resource(lock); //一个重要的资源,应该一次只能由一个线程访问
    
    void doStuff() {
        lock.lock();
        
        try {
            if (!res.initialized.get()) //检查资源之前是否未初始化
            {
                res.init(); //为初始化启动一个单独的线程
    
                wait.await(); //如果资源未初始化,则使达到此点的任何线程等待
            }
    
            res.use(); //这里是我不明白的地方。假设有5个线程在前一个语句中停放,那么当调用signalAll()时,不会所有5个线程同时唤醒并在此处恢复执行并同时使用此资源并破坏此资源吗?但事实并非如此,为什么?
        } finally {
            lock.unlock();
        }
    }    

    private static final class Resource extends Thread {
        private final ReentrantLock lock;
        private final Condition init;
    
        private final AtomicBoolean 
        started = new AtomicBoolean(false), 
        initialized = new AtomicBoolean(false);
      
        private Resource(ReentrantLock lock) {
            this.lock = lock;
            this.init = lock.newCondition();
        }
      
        private void init() {
            if (!initialized.get()) {
                if (!started.get()) {
                    start();
         
                    started.set(true);
                }
    
                while (!initialized.get()) {
                    init.awaitUninterruptibly();
                } //在出现虚假唤醒的情况下,重复检查
            }
        }
    
        private void use() {} //重要的内容
    
        private int get() { return 5; }
      
        @Override
        public void run() {
            lock.lock();
            try {
                TimeUnit.SECONDS.sleep(5);
        
                initialized.set(true);
        
                init.signalAll(); //在上面的示例中,这应该同时唤醒所有5个线程并破坏关键部分,但实际情况并非如此,为什么?
            } catch (InterruptedException ex) {}
            finally {
                lock.unlock();
            }
        }
    }

只使用signal()只有一个线程会唤醒并在关键部分恢复执行因此不会破坏任何内容但是使用signalAll()多个线程会在停放的点上恢复执行即在关键部分内部),那么为什么不会破坏呢在什么时候/何处应该使用每个方法即最佳做法
英文:

I am trying to understand How does signalAll() not break the critical section for example

//Some Class Here is Trying to use an resource which is to to initialized in an seperate thread
ReentrantLock lock=new ReentrantLock();
Condition wait=lock.newCondition();
Resource res=new Resource(lock); //An Important Resource Which Should Be Accessed Only By One Thread At A Time
void doStuff()
{
lock.lock();
try
{
if(!res.initialized.get()) //Check If Resource Was Previously Not Initialized
{
res.init();//Spawns An Seperate Thread For Initialization
wait.await();//If Resource Is Not Initialized Make Whatever Thread Which Reached This Point Wait
} 
res.use(); //Here Is What I Don't Understand If Let's Say 5 threads were parked in the previous statement then when signalAll() is called won't all 5 threads wake up simultaneously and resume execution at this point and use this resource at the same time and break this resource? But it dosen't turn out like that why?
}
}
finally{lock.unlock();}
}    
private static final class Resource extends Thread
{
private final ReentrantLock lock;
private final Condition init;
private final AtomicBoolean 
started=new AtomicBoolean(false), 
initialized=new AtomicBoolean(false);
private Resource(ReentrantLock lock)
{
this.lock=lock;
this.init=lock.newCondition();
}
private void init()
{
if(!initialized.get())
{
if(!started.get())
{
start();
started.set(true);
}
while(!initialized.get()){init.awaitUninterruptibly();}//In case of spurrous wakeups check repeatedlly
}
}
private void use(){}//Important Stuff  
private int get(){return 5;}
@Override
public void run()
{
lock.lock();
try
{
TimeUnit.SECONDS.sleep(5);
initialized.set(true);
init.signalAll(); //This should in the above example wake up all 5 threads simultaneously and break the critical section but that does not happen how?
}
catch(InterruptedException ex){}
finally{lock.unlock();}
}
}

With just signal() only one thread wakes up and resumes execution at the critical section so nothing breaks but with signalAll() multiple threads resumes execution at the point it was parked[i.e inside the critical section] so how does nothing break? and when/where should we use each i.e best practises

答案1

得分: 2

The short answer:

await()不仅挂起当前线程,还释放锁。signalAll()唤醒所有挂起的线程,但每个线程在await()调用可以返回之前都必须重新获取锁。通过这种方式,即使调用了notifyAll(),关键部分也只能在之前获取锁的线程释放锁之后才能被另一个线程进入。

The long answer:

为了更好地理解 - 让我们假设在Java中不存在await()、singal()或signalAll()。你如何等待资源的异步初始化?你的代码可能会像这样:

void doStuff(Resource resource) throws InterruptedException {

  lock.lock();
    try {
      while (!resource.isInitialized()) {
        resource.startAsyncInit();
        lock.unlock();
        Thread.sleep(100);
        lock.lock();
    }
    doSomethingWith(resource);
  } finally {
    lock.unlock();
  }
}

但这会有以下缺点:

每个等待初始化的线程都会一遍又一遍地被挂起和唤醒。
每个单独的线程都会一遍又一遍地获取锁并释放锁。
你越频繁地这样做,等待的线程越多,这个过程就会越昂贵。这种忙碌的等待会消耗CPU。如果没有明确的睡眠,代码很容易导致CPU使用率达到100%。

await()允许你用基于信号的机制替换忙碌等待。所有等待的线程都被挂起,在等待时不消耗任何CPU。只有偶尔发生的虚假唤醒(至少在某些系统上)可能会消耗一些CPU。

何时通常使用singal()、signalAll():

唤醒线程和挂起线程并非免费,随着线程数量的增加而变得更加昂贵。如果你有一个资源在并发使用之前必须进行初始化,那么通过调用signalAll()一次性唤醒所有线程是有意义的。

但想象一下具有多个消费者线程和多个生产者线程的消费者/生产者模式,其中单个生产者线程只提供一个工作项,由一个消费者线程处理。在这种情况下,单个生产者线程只唤醒一个消费者线程会更有意义,而不是唤醒所有线程。否则,所有被唤醒的线程都会首先竞争获取单个工作项,一个线程会获胜,其他所有线程都必须再次进入睡眠状态。每次生产单个工作项时都必须重复此过程。当在短时间内产生大量工作项时,你最终会失去信号机制的所有优势。大部分线程都会一遍又一遍地被挂起和唤醒,它们会不断竞争同一个工作项,最终会出现几乎与上述忙碌等待示例相同但没有明确睡眠的开销;-)

在你的示例中signal()与signalAll()的区别:

当第一个线程获得锁并调用init()方法时,它启动了初始化线程,然后在调用awaitUninterruptibly()时释放了锁。与此同时,初始化线程试图获取锁,但在调用awaitUninterruptibly()之前将无法获取锁。

锁默认情况下是不公平的。这意味着最长等待的线程不保证会首先获得锁。当实际调用awaitUninterruptibly()并释放锁时,其他线程可能已经在此期间通过调用lock()方法尝试获取锁。即使初始化线程首先尝试获取锁,也不能保证它会在任何其他线程之前获取锁。在初始化线程之前获取锁的每个其他线程都能够调用await()方法。如果你在初始化线程中只调用singal(),那么能够到达await()调用的所有线程都永远不会被唤醒,将永远处于睡眠状态。为了避免这种情况,在你的示例中使用singalAll()是必不可少的。另一种可能性是使用“公平”锁(参见ReentrantLock的JavaDoc)。使用“公平”锁是否调用singal()或signalAll()应该没有区别。但是由于“公平”锁的开销相当大,我建议保持不公平锁并使用singalAll()。

是否会出现一些线程永久睡眠的情况取决于正确的时机。因此,你可能在一个主机上运行这段代码数百次而没有任何问题,但在其他主机上频繁遇到这种情况。更糟糕的是,在存在虚假唤醒的环境中,你只会偶尔遇到这种情况;-)

英文:

The short answer:

await() does not only suspend the current thread, it also releases the lock. signalAll() wakes up all suspended threads but each thread has to re-acquire the lock before the await() call can return. With it, even after calling notifyAll() the critical section can only be entered by a thread after the thread that acquired the lock before relinquishes the lock.

The long answer:

For better understanding - let's pretend that neither await(), singal() nor signalAll() would exist in Java. How would you wait for the asynchronous initialization of your resource? Your code would probably look something like this:

void doStuff(Resource resource) throws InterruptedException {
lock.lock();
try {
while (!resource.isInitialized()) {
resource.startAsyncInit();
lock.unlock();
Thread.sleep(100);
lock.lock();
}
doSomethingWith(resource);
} finally {
lock.unlock();
}
}

But this would have the following drawback:

Each thread that waits for initialization is suspended and woken up again and again.
Each single thread acquires the lock and releases the lock again and again.
The more frequent you do this and the more threads are waiting the more expensive
does it get. This busy waiting consumes CPU. Without an explicit sleep the code would easily
lead to 100% CPU usage.

await() allows you to replace the busy waiting with a signal-based mechanism.
All waiting threads are suspended and do not consume any CPU while they are waiting.
Only sporadically occuring spurious wake-ups (at least on some systems) may consume some CPU.

When to use singal(), signallAll() in general:

Waking up a thread and suspending a thread is not for free and gets more expensive with
the number of threads. If you have a resource that has to be initialized
before it can be used concurrently by all threads it makes sense to wake up all threads at once by calling signalAll().

But think of a consumer/producer pattern with multiple consumer threads and multiple producer threads where a single producer thread provides only one work item that is processed by one consumer thread. In this case it would make much more sense that a producer thread wakes up only one consumer thread instead of all. Otherwise all awakened threads would first compete for the single work item, one would win and all others would have to be sent back to sleep again. This would have to be repeated every time when a single work item is produced. When a lot of work items are produced in short time
you would finally loose all the advantage of the singalling. The majority of threads would be suspended and woken up again and again, they would compete for one single item again and again and you would finally end up with nearly the same overhead as the example above with busy waiting but without an explicit sleep ReentrantLock的Condition如何工作signallAll()?

signal() vs signalAll() in your example:

When the first thread gets the hold of the lock it calls the init() method, starts the thread for initialization and then releases the lock when it calls awaitUninterruptibly(). The initialization thread in the meanwhile tries to acquire the lock but it will not get it until awaitUninterruptibly() is called.

Locks are by default unfair. This means that it is not guaranteed that the longest-waiting thread will get the lock first. When awaitUninterruptibly() is actually called and the lock is released other theads may have already tried to acquire the lock in the meanwhile by calling the lock() method. Even when your initialization thread tried to acquire the lock first, it is not guaranteed that it will get the lock before any other thread. Every other thread that will get the lock before your initialization thread will be able to call the await() method. If you then only call singal() in your initialization thread all threads that were able to get to the await() call would never be woken up and sleep forever. To avoid this, it is essential to use singalAll() in your example. Another possibility would be to use a "fair" lock (see the JavaDoc of the ReentrantLock). With a "fair" lock it shouldn't make any difference whether you call singal() or signalAll(). But since "fair" locks have quite an overhead I would suggest to keep the unfair lock and use singalAll().

Whether you run into a situation where some threads sleep forever depends on the right timing. So you propably may run this code on one host hundreds of times without any problem but run into this situation frequently on other hosts. Even worse, in environments with spurious wake-ups you would run into this only from time to time ReentrantLock的Condition如何工作signallAll()?

huangapple
  • 本文由 发表于 2020年8月23日 21:29:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/63547495.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定