Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit aced5a9

Browse files
committed
Rewrite ConditionVariableBroadcast() to avoid live-lock.
The original implementation of ConditionVariableBroadcast was, per its self-description, "the dumbest way possible". Thomas Munro found out it was a bit too dumb. An awakened process may immediately re-queue itself, if the specific condition it's waiting for is not yet satisfied. If this happens before ConditionVariableBroadcast is able to see the wait queue as empty, then ConditionVariableBroadcast will re-awaken the same process, repeating the cycle. Given unlucky timing this back-and-forth can repeat indefinitely; loops lasting thousands of seconds have been seen in testing. To fix, add our own process to the end of the wait queue to serve as a sentinel, and exit the broadcast loop once our process is not there anymore. There are various special considerations described in the comments, the principal disadvantage being that wakers can no longer be sure whether they awakened a real waiter or just a sentinel. But in practice nobody pays attention to the result of ConditionVariableSignal or ConditionVariableBroadcast anyway, so that problem seems hypothetical. Back-patch to v10 where condition_variable.c was introduced. Tom Lane and Thomas Munro Discussion: https://postgr.es/m/CAEepm=0NWKehYw7NDoUSf8juuKOPRnCyY3vuaSvhrEWsOTAa3w@mail.gmail.com
1 parent 19c47e7 commit aced5a9

File tree

1 file changed

+77
-5
lines changed

1 file changed

+77
-5
lines changed

src/backend/storage/lmgr/condition_variable.c

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,87 @@ int
214214
ConditionVariableBroadcast(ConditionVariable *cv)
215215
{
216216
int nwoken = 0;
217+
int pgprocno = MyProc->pgprocno;
218+
PGPROC *proc = NULL;
219+
bool have_sentinel = false;
220+
221+
/*
222+
* In some use-cases, it is common for awakened processes to immediately
223+
* re-queue themselves. If we just naively try to reduce the wakeup list
224+
* to empty, we'll get into a potentially-indefinite loop against such a
225+
* process. The semantics we really want are just to be sure that we have
226+
* wakened all processes that were in the list at entry. We can use our
227+
* own cvWaitLink as a sentinel to detect when we've finished.
228+
*
229+
* A seeming flaw in this approach is that someone else might signal the
230+
* CV and in doing so remove our sentinel entry. But that's fine: since
231+
* CV waiters are always added and removed in order, that must mean that
232+
* every previous waiter has been wakened, so we're done. We'll get an
233+
* extra "set" on our latch from the someone else's signal, which is
234+
* slightly inefficient but harmless.
235+
*
236+
* We can't insert our cvWaitLink as a sentinel if it's already in use in
237+
* some other proclist. While that's not expected to be true for typical
238+
* uses of this function, we can deal with it by simply canceling any
239+
* prepared CV sleep. The next call to ConditionVariableSleep will take
240+
* care of re-establishing the lost state.
241+
*/
242+
ConditionVariableCancelSleep();
217243

218244
/*
219-
* Let's just do this the dumbest way possible. We could try to dequeue
220-
* all the sleepers at once to save spinlock cycles, but it's a bit hard
221-
* to get that right in the face of possible sleep cancelations, and we
222-
* don't want to loop holding the mutex.
245+
* Inspect the state of the queue. If it's empty, we have nothing to do.
246+
* If there's exactly one entry, we need only remove and signal that
247+
* entry. Otherwise, remove the first entry and insert our sentinel.
223248
*/
224-
while (ConditionVariableSignal(cv))
249+
SpinLockAcquire(&cv->mutex);
250+
/* While we're here, let's assert we're not in the list. */
251+
Assert(!proclist_contains(&cv->wakeup, pgprocno, cvWaitLink));
252+
253+
if (!proclist_is_empty(&cv->wakeup))
254+
{
255+
proc = proclist_pop_head_node(&cv->wakeup, cvWaitLink);
256+
if (!proclist_is_empty(&cv->wakeup))
257+
{
258+
proclist_push_tail(&cv->wakeup, pgprocno, cvWaitLink);
259+
have_sentinel = true;
260+
}
261+
}
262+
SpinLockRelease(&cv->mutex);
263+
264+
/* Awaken first waiter, if there was one. */
265+
if (proc != NULL)
266+
{
267+
SetLatch(&proc->procLatch);
225268
++nwoken;
269+
}
270+
271+
while (have_sentinel)
272+
{
273+
/*
274+
* Each time through the loop, remove the first wakeup list entry, and
275+
* signal it unless it's our sentinel. Repeat as long as the sentinel
276+
* remains in the list.
277+
*
278+
* Notice that if someone else removes our sentinel, we will waken one
279+
* additional process before exiting. That's intentional, because if
280+
* someone else signals the CV, they may be intending to waken some
281+
* third process that added itself to the list after we added the
282+
* sentinel. Better to give a spurious wakeup (which should be
283+
* harmless beyond wasting some cycles) than to lose a wakeup.
284+
*/
285+
proc = NULL;
286+
SpinLockAcquire(&cv->mutex);
287+
if (!proclist_is_empty(&cv->wakeup))
288+
proc = proclist_pop_head_node(&cv->wakeup, cvWaitLink);
289+
have_sentinel = proclist_contains(&cv->wakeup, pgprocno, cvWaitLink);
290+
SpinLockRelease(&cv->mutex);
291+
292+
if (proc != NULL && proc != MyProc)
293+
{
294+
SetLatch(&proc->procLatch);
295+
++nwoken;
296+
}
297+
}
226298

227299
return nwoken;
228300
}

0 commit comments

Comments
 (0)