thread: add generic futex-based muteces
authorRémi Denis-Courmont <remi@remlab.net>
Wed, 19 Feb 2020 19:09:06 +0000 (21:09 +0200)
committerRémi Denis-Courmont <remi@remlab.net>
Fri, 21 Feb 2020 17:15:24 +0000 (19:15 +0200)
This provides a common implementation of fast (non-debug),
error-checking (debug) and recursive muteces on top of
vlc_atomic_wait() and vlc_atomic_notify_one(), using Drepper's tristate
mutex algorithm.

Benefits of this implementation include:
 - Error checking is supported on all platforms (including Windows).
 - Initialization can never fail, is not subject to aborts.
 - Destruction is a no-op and can be removed altogether.
 - Static muteces do not leak or need kludges.
 - Ownership can be checked directly without the mutex mark system.
 - Non-ownership can be checked in assertion.

Because the player code uses the same vlc_mutex_t typedef for both
non-recursive and recursive usages, disentanglement is not possible.
This patchset thus supports both semantics together as before.

include/vlc_threads.h
src/misc/threads.c

index 836f042..598e6fb 100644 (file)
@@ -77,6 +77,7 @@ typedef struct
     };
 } vlc_mutex_t;
 #define VLC_STATIC_MUTEX { false, { { false, 0 } } }
+#define LIBVLC_DONT_WANT_MUTEX
 #define LIBVLC_NEED_RWLOCK
 typedef INIT_ONCE vlc_once_t;
 #define VLC_STATIC_ONCE INIT_ONCE_STATIC_INIT
@@ -125,6 +126,7 @@ typedef struct
     };
 } vlc_mutex_t;
 #define VLC_STATIC_MUTEX { false, { { false, 0 } } }
+#define LIBVLC_DONT_WANT_MUTEX
 #define LIBVLC_NEED_RWLOCK
 typedef struct
 {
@@ -180,6 +182,7 @@ typedef pthread_t vlc_osthread_t;
 #define vlc_thread_equal(a,b) pthread_equal(a,b)
 typedef pthread_mutex_t vlc_mutex_t;
 #define VLC_STATIC_MUTEX PTHREAD_MUTEX_INITIALIZER
+#define LIBVLC_DONT_WANT_MUTEX
 typedef pthread_once_t  vlc_once_t;
 #define VLC_STATIC_ONCE   PTHREAD_ONCE_INIT
 typedef pthread_key_t   vlc_threadvar_t;
@@ -227,6 +230,7 @@ typedef pthread_t       vlc_osthread_t;
 #define vlc_thread_equal(a,b) pthread_equal(a,b)
 typedef pthread_mutex_t vlc_mutex_t;
 #define VLC_STATIC_MUTEX PTHREAD_MUTEX_INITIALIZER
+#define LIBVLC_DONT_WANT_MUTEX
 typedef pthread_rwlock_t vlc_rwlock_t;
 #define VLC_STATIC_RWLOCK PTHREAD_RWLOCK_INITIALIZER
 typedef pthread_once_t  vlc_once_t;
@@ -271,21 +275,9 @@ typedef struct
 typedef pthread_t vlc_osthread_t;
 #define vlc_thread_equal(a,b) pthread_equal(a,b)
 
-/**
- * Mutex.
- *
- * Storage space for a mutual exclusion lock.
- *
- * \ingroup mutex
- */
 typedef pthread_mutex_t vlc_mutex_t;
-
-/**
- * Static initializer for (static) mutex.
- *
- * \ingroup mutex
- */
 #define VLC_STATIC_MUTEX PTHREAD_MUTEX_INITIALIZER
+#define LIBVLC_DONT_WANT_MUTEX
 
 /**
  * Read/write lock.
@@ -343,6 +335,43 @@ typedef struct vlc_timer *vlc_timer_t;
  * \defgroup mutex Mutual exclusion locks
  * @{
  */
+#ifndef LIBVLC_DONT_WANT_MUTEX
+/**
+ * Mutex.
+ *
+ * Storage space for a mutual exclusion lock.
+ */
+typedef struct
+{
+    union {
+#ifndef __cplusplus
+        struct {
+            atomic_uint value;
+            atomic_uint recursion;
+            const void *_Atomic owner;
+        };
+#endif
+        struct {
+            unsigned int value;
+            unsigned int recursion;
+            const void *owner;
+        } dummy;
+    };
+} vlc_mutex_t;
+
+/**
+ * Static initializer for (static) mutex.
+ *
+ * \note This only works in C code.
+ * In C++, consider using a global \ref vlc::threads::mutex instance instead.
+ */
+#define VLC_STATIC_MUTEX { \
+    .value = ATOMIC_VAR_INIT(0), \
+    .recursion = ATOMIC_VAR_INIT(0), \
+    .owner = ATOMIC_VAR_INIT(NULL), \
+}
+#endif
+
 /**
  * Initializes a fast mutex.
  *
index bdb3c9d..3576874 100644 (file)
@@ -201,6 +201,130 @@ void (vlc_tick_sleep)(vlc_tick_t delay)
 }
 #endif
 
+#ifndef LIBVLC_DONT_WANT_MUTEX
+static void vlc_mutex_init_common(vlc_mutex_t *mtx, bool recursive)
+{
+    atomic_init(&mtx->value, 0);
+    atomic_init(&mtx->recursion, recursive);
+    atomic_init(&mtx->owner, NULL);
+}
+
+void vlc_mutex_init(vlc_mutex_t *mtx)
+{
+    vlc_mutex_init_common(mtx, false);
+}
+
+void vlc_mutex_init_recursive(vlc_mutex_t *mtx)
+{
+    vlc_mutex_init_common(mtx, true);
+}
+
+void vlc_mutex_destroy(vlc_mutex_t *mtx)
+{
+    assert(atomic_load_explicit(&mtx->value, memory_order_relaxed) == 0);
+    assert(atomic_load_explicit(&mtx->recursion, memory_order_relaxed) <= 1);
+    assert(atomic_load_explicit(&mtx->owner, memory_order_relaxed) == NULL);
+    (void) mtx;
+}
+
+static _Thread_local char thread_self[1];
+#define THREAD_SELF ((const void *)thread_self)
+
+static bool vlc_mutex_held(const vlc_mutex_t *mtx)
+{
+    /* This comparison is thread-safe:
+     * Even though other threads may modify the owner field at any time,
+     * they will never make ti compare equal to the calling thread.
+     */
+    return THREAD_SELF == atomic_load_explicit(&mtx->owner,
+                                               memory_order_relaxed);
+}
+
+void vlc_mutex_lock(vlc_mutex_t *mtx)
+{
+    unsigned value;
+
+    /* This is the Drepper (non-recursive) mutex algorithm
+     * from his "Futexes are tricky" paper. The mutex can value be:
+     * - 0: the mutex is free
+     * - 1: the mutex is locked and uncontended
+     * - 2: the mutex is contended (i.e., unlock needs to wake up a waiter)
+     */
+    if (vlc_mutex_trylock(mtx) == 0)
+        return;
+
+    int canc = vlc_savecancel(); /* locking is never a cancellation point */
+
+    while ((value = atomic_exchange_explicit(&mtx->value, 2,
+                                             memory_order_acquire)) != 0)
+        vlc_atomic_wait(&mtx->value, 2);
+
+    vlc_restorecancel(canc);
+    atomic_store_explicit(&mtx->owner, THREAD_SELF, memory_order_relaxed);
+    vlc_mutex_mark(mtx);
+}
+
+int vlc_mutex_trylock(vlc_mutex_t *mtx)
+{
+    /* Check the recursion counter:
+     * - 0: mutex is not recursive.
+     * - 1: mutex is recursive but free or locked non-recursively.
+     * - n > 1: mutex is recursive and locked n time(s).
+     */
+    unsigned recursion = atomic_load_explicit(&mtx->recursion,
+                                              memory_order_relaxed);
+    if (unlikely(recursion) && vlc_mutex_held(mtx)) {
+        /* This thread already owns the mutex, locks recursively.
+         * Other threads shall not have modified the recursion or owner fields.
+         */
+        atomic_store_explicit(&mtx->recursion, recursion + 1,
+                              memory_order_relaxed);
+        vlc_mutex_mark(mtx);
+        return 0;
+    } else
+        assert(!vlc_mutex_held(mtx));
+
+    unsigned value = 0;
+
+    if (atomic_compare_exchange_strong_explicit(&mtx->value, &value, 1,
+                                                memory_order_acquire,
+                                                memory_order_relaxed)) {
+        atomic_store_explicit(&mtx->owner, THREAD_SELF, memory_order_relaxed);
+        vlc_mutex_mark(mtx);
+        return 0;
+    }
+
+    return EBUSY;
+}
+
+void vlc_mutex_unlock(vlc_mutex_t *mtx)
+{
+    assert(vlc_mutex_held(mtx));
+
+    unsigned recursion = atomic_load_explicit(&mtx->recursion,
+                                              memory_order_relaxed);
+    if (unlikely(recursion > 1)) {
+        /* Non-last recursive unlocking. */
+        atomic_store_explicit(&mtx->recursion, recursion - 1,
+                              memory_order_relaxed);
+        vlc_mutex_unmark(mtx);
+        return;
+    }
+
+    atomic_store_explicit(&mtx->owner, NULL, memory_order_relaxed);
+    vlc_mutex_unmark(mtx);
+
+    switch (atomic_exchange_explicit(&mtx->value, 0, memory_order_release)) {
+        case 2:
+            vlc_atomic_notify_one(&mtx->value);
+        case 1:
+            break;
+        default:
+            vlc_assert_unreachable();
+    }
+}
+#endif
+
 void vlc_cond_init(vlc_cond_t *cond)
 {
     cond->head = NULL;