[vlc-devel] [PATCH 2/3] thread: use a linked-list in condition variables

RĂ©mi Denis-Courmont remi at remlab.net
Mon Feb 17 21:54:08 CET 2020


With more than one thread waiting on the same condition variable, the
lock-step logic could (at least theoretically) lose a wake-up in 2**32.

This patch uses a linked-list instead, with one atomic variable per
waiting thread, so we know exactly which threads are woken up
(musl uses a similar strategy for non-shared condition variables.)

Notes:
- A singly linked list is inadequate here, as a thread may need to
  remove itself from the list on time-out or spurious wake-ups from
  atomic_wait().
- vlc_list cannot be used here as it cannot sustain the definition of
  VLC_STATIC_COND. Furthermore, vlc_list undefines the list node on
  removal, which is inappropriate as per the previous note.
---
 include/vlc_threads.h |  12 ++-
 src/misc/threads.c    | 173 +++++++++++++++++++++++++++++-------------
 2 files changed, 124 insertions(+), 61 deletions(-)

diff --git a/include/vlc_threads.h b/include/vlc_threads.h
index f52e572e6e..a1db42df06 100644
--- a/include/vlc_threads.h
+++ b/include/vlc_threads.h
@@ -343,16 +343,14 @@ typedef struct vlc_timer *vlc_timer_t;
 #endif
 
 #ifdef LIBVLC_NEED_CONDVAR
+struct vlc_cond_waiter;
+
 typedef struct
 {
-    union {
-#ifndef __cplusplus
-        atomic_uint value;
-#endif
-        int        cpp_value;
-    };
+    struct vlc_cond_waiter *head;
+    vlc_mutex_t lock;
 } vlc_cond_t;
-# define VLC_STATIC_COND { 0 }
+# define VLC_STATIC_COND { NULL, VLC_STATIC_MUTEX }
 #endif
 
 /**
diff --git a/src/misc/threads.c b/src/misc/threads.c
index 3c21a8b36b..debb66bdc8 100644
--- a/src/misc/threads.c
+++ b/src/misc/threads.c
@@ -203,8 +203,8 @@ void (vlc_tick_sleep)(vlc_tick_t delay)
 #ifdef LIBVLC_NEED_CONDVAR
 void vlc_cond_init(vlc_cond_t *cond)
 {
-    /* Initial value is irrelevant but set it for happy debuggers */
-    atomic_init(&cond->value, 0);
+    cond->head = NULL;
+    vlc_mutex_init(&cond->lock);
 }
 
 void vlc_cond_init_daytime(vlc_cond_t *cond)
@@ -214,80 +214,145 @@ void vlc_cond_init_daytime(vlc_cond_t *cond)
 
 void vlc_cond_destroy(vlc_cond_t *cond)
 {
-    /* Tempting sanity check but actually incorrect:
-    assert((atomic_load_explicit(&cond->value,
-                                 memory_order_relaxed) & 1) == 0);
-     * Due to timeouts and spurious wake-ups, the futex value can look like
-     * there are waiters, even though there are none. */
-    (void) cond;
+    assert(cond->head == NULL);
+    vlc_mutex_destroy(&cond->lock);
+}
+
+struct vlc_cond_waiter {
+    struct vlc_cond_waiter **pprev, *next;
+    atomic_uint value;
+    vlc_cond_t *cond;
+    vlc_mutex_t *mutex;
+};
+
+static void vlc_cond_signal_waiter(struct vlc_cond_waiter *waiter)
+{
+    waiter->pprev = &waiter->next;
+    waiter->next = NULL;
+    atomic_fetch_add_explicit(&waiter->value, 1, memory_order_relaxed);
+    vlc_atomic_notify_one(&waiter->value);
 }
 
 void vlc_cond_signal(vlc_cond_t *cond)
 {
-    /* Probably the best documented approach is that of Bionic: increment
-     * the futex here, and simply load the value in cnd_wait(). This has a bug
-     * as unlikely as well-known: signals get lost if the futex is incremented
-     * an exact multiple of 2^(CHAR_BIT * sizeof (int)) times.
-     *
-     * A different presumably bug-free solution is used here:
-     * - cnd_signal() sets the futex to the equal-or-next odd value, while
-     * - cnd_wait() sets the futex to the equal-or-next even value.
-     **/
-    atomic_fetch_or_explicit(&cond->value, 1, memory_order_relaxed);
-    vlc_atomic_notify_one(&cond->value);
+    struct vlc_cond_waiter *waiter;
+
+    /* Some call sites signal their condition variable without holding the
+     * corresponding lock. Thus an extra lock is needed here to ensure the
+     * consistency of the linked list and the lifetime of its elements.
+     * If all call sites locked cleanly, the inner lock would be unnecessary.
+     */
+    vlc_mutex_lock(&cond->lock);
+    waiter = cond->head;
+
+    if (waiter != NULL) {
+        struct vlc_cond_waiter *next = waiter->next;
+        struct vlc_cond_waiter **pprev = waiter->pprev;
+
+        *pprev = next;
+
+        if (next != NULL)
+            next->pprev = pprev;
+
+        vlc_cond_signal_waiter(waiter);
+    }
+
+    vlc_mutex_unlock(&cond->lock);
 }
 
 void vlc_cond_broadcast(vlc_cond_t *cond)
 {
-    atomic_fetch_or_explicit(&cond->value, 1, memory_order_relaxed);
-    vlc_atomic_notify_all(&cond->value);
+    struct vlc_cond_waiter *waiter;
+
+    vlc_mutex_lock(&cond->lock);
+    waiter = cond->head;
+    cond->head = NULL;
+
+    /* Keep the lock here so that waiters don't go out of scope */
+    while (waiter != NULL) {
+        struct vlc_cond_waiter *next = waiter->next;
+
+        vlc_cond_signal_waiter(waiter);
+        waiter = next;
+    }
+
+    vlc_mutex_unlock(&cond->lock);
 }
 
-void vlc_cond_wait(vlc_cond_t *cond, vlc_mutex_t *mutex)
+static void vlc_cond_wait_prepare(struct vlc_cond_waiter *waiter,
+                                  vlc_cond_t *cond, vlc_mutex_t *mutex)
 {
-    unsigned value = atomic_load_explicit(&cond->value,
-                                     memory_order_relaxed);
-    while (value & 1)
-    {
-        if (atomic_compare_exchange_weak_explicit(&cond->value, &value,
-                                                  value + 1,
-                                                  memory_order_relaxed,
-                                                  memory_order_relaxed))
-            value++;
-    }
+    struct vlc_cond_waiter *next;
+
+    waiter->pprev = &cond->head;
+    atomic_init(&waiter->value, 0);
+    waiter->cond = cond;
+    waiter->mutex = mutex;
+
+    vlc_mutex_lock(&cond->lock);
+    next = cond->head;
+    cond->head = waiter;
+    waiter->next = next;
+
+    if (next != NULL)
+        next->pprev = &waiter->next;
 
-    vlc_cancel_addr_prepare(&cond->value);
+    vlc_mutex_unlock(&cond->lock);
+    vlc_cancel_addr_prepare(&waiter->value);
     vlc_mutex_unlock(mutex);
+}
+
+static void vlc_cond_wait_finish(struct vlc_cond_waiter *waiter,
+                                 vlc_cond_t *cond, vlc_mutex_t *mutex)
+{
+    struct vlc_cond_waiter *next;
 
-    vlc_atomic_wait(&cond->value, value);
+    /* If this waiter is still on the linked list, remove it before it goes
+     * out of scope. Otherwise, this is a no-op.
+     */
+    vlc_mutex_lock(&cond->lock);
+    next = waiter->next;
+    *(waiter->pprev) = next;
 
+    if (next != NULL)
+        next->pprev = waiter->pprev;
+
+    vlc_mutex_unlock(&cond->lock);
+
+    /* Lock the caller's mutex as required by condition variable semantics. */
     vlc_mutex_lock(mutex);
-    vlc_cancel_addr_finish(&cond->value);
+    vlc_cancel_addr_finish(&waiter->value);
 }
 
-int vlc_cond_timedwait(vlc_cond_t *cond, vlc_mutex_t *mutex,
-                       vlc_tick_t deadline)
+static void vlc_cond_wait_cleanup(void *data)
 {
-    unsigned value = atomic_load_explicit(&cond->value, 
-                                          memory_order_relaxed);
-    int ret;
+    struct vlc_cond_waiter *waiter = data;
 
-    while (value & 1)
-    {
-        if (atomic_compare_exchange_weak_explicit(&cond->value, &value,
-                                                  value + 1,
-                                                  memory_order_relaxed,
-                                                  memory_order_relaxed))
-            value++;
-    }
+    vlc_cond_wait_finish(waiter, waiter->cond, waiter->mutex);
+}
 
-    vlc_cancel_addr_prepare(&cond->value);
-    vlc_mutex_unlock(mutex);
+void vlc_cond_wait(vlc_cond_t *cond, vlc_mutex_t *mutex)
+{
+    struct vlc_cond_waiter waiter;
 
-    ret = vlc_atomic_timedwait(&cond->value, value, deadline);
+    vlc_cond_wait_prepare(&waiter, cond, mutex);
+    vlc_cleanup_push(vlc_cond_wait_cleanup, &waiter);
+    vlc_atomic_wait(&waiter.value, 0);
+    vlc_cleanup_pop();
+    vlc_cond_wait_cleanup(&waiter);
+}
 
-    vlc_mutex_lock(mutex);
-    vlc_cancel_addr_finish(&cond->value);
+int vlc_cond_timedwait(vlc_cond_t *cond, vlc_mutex_t *mutex,
+                       vlc_tick_t deadline)
+{
+    struct vlc_cond_waiter waiter;
+    int ret;
+
+    vlc_cond_wait_prepare(&waiter, cond, mutex);
+    vlc_cleanup_push(vlc_cond_wait_cleanup, &waiter);
+    ret = vlc_atomic_timedwait(&waiter.value, 0, deadline);
+    vlc_cleanup_pop();
+    vlc_cond_wait_cleanup(&waiter);
 
     return ret;
 }
@@ -296,7 +361,7 @@ int vlc_cond_timedwait_daytime(vlc_cond_t *cond, vlc_mutex_t *mutex,
                                time_t deadline_daytime)
 {
     struct timespec ts;
-    vlc_tick_t deadline = vlc_tick_from_sec( deadline_daytime );
+    vlc_tick_t deadline = vlc_tick_from_sec(deadline_daytime);
 
     timespec_get(&ts, TIME_UTC);
     /* real-time to monotonic timestamp conversion */
-- 
2.25.0



More information about the vlc-devel mailing list