input: add per-thread sleep interruption framework
authorRémi Denis-Courmont <remi@remlab.net>
Tue, 30 Jun 2015 20:52:51 +0000 (23:52 +0300)
committerRémi Denis-Courmont <remi@remlab.net>
Wed, 1 Jul 2015 16:17:53 +0000 (19:17 +0300)
For the sake of simplicity and for historical reasons, access and demux
modules perform I/O in blocking mode. If no data is available (or more
generally no I/O events), the blocking I/O calls will sleep and hold
the whole input thread. This can lead to long time-outs or even
complete deadlocks, e.g. notably in case of network error.

Originally, a volatile flag (b_die) was checked at frequent interval to
ascertain whether to abort. This violated the threaded memory model,
and was incompatible with race-to-idle power management.

In 2007, the VLC object thread signaling functions were introduced
(vlc_object_wait, vlc_object_signal, ...) in an attempt to solve this.
They proved inflexible and were not compatible with poll/select-style
I/O events multiplexing. Those functions were ultimately removed a
little over a year later.

In the mean time, the "wait pipe" had been introduced. It was focused
on network/socket data reception. While it continues to be used, it
suffers several limitations:
 - it affects other threads using the same VLC object,
   and indistinctly disrupts all I/O after the "kill",
 - it incorrectly assumes that the same VLC object is used everywhere
   (leading to race conditions and live loops),
 - the convenience wrappers around the wait pipe can only wait on one
   single I/O event direction on a single file descriptor at a time,
 - it is currently tied to the VLC input thread.

Also at about the same time, thread cancellation was reintroduced.
Thread cancellation has proven helpful for simple thread main loops.
But it ranges from impractical to unusable when sleeping deep within
layers of code, such as in access and stream modules.

Generally the problem of interrupting I/O is an intractable halting
problem. And in practice a given reading operations inside a demuxer
cannot be interrupted without breaking the state machine of the
demuxer - in many or most cases. This changes set is only an attempt
to complement thread cancellation, This does overcome most limitations
of the existing "wait pipe" system and of former VLC object signals:

 - It is triggered by a function call specifying a target context.
 The context is tied to the thread that needs to be woken up from
 sleep. This works quite well because the problem essentially relates
 to the call flow of the sleeping thread. On the trigger side, this is
 similar to thread cancellation.

 - It leaves some flexibility w.r.t. choice of sleeping primitives.
 This initial change uses semaphores. Low-level file I/O will be
 introduced later.

 - The wake-up mechanism is edge-triggered and can be fired multiple
 times. Thus it does not irreversibly prevent all I/O and sleeping
 operations once fired. It only interrupts the ongoing or next sleep.
 In principles non-fatal interruptions could be handled that way, for
 instance input thread seek (rather than forceful stop) although that
 is not part of the changes set.

 - It is not tied to any specific event. The initial use case is
 stopping the input thread and checking vlc_object_alive() but it can
 be used for other purposes.

include/vlc_interrupt.h [new file with mode: 0644]
src/Makefile.am
src/libvlccore.sym
src/misc/interrupt.c [new file with mode: 0644]
src/misc/interrupt.h [new file with mode: 0644]

diff --git a/include/vlc_interrupt.h b/include/vlc_interrupt.h
new file mode 100644 (file)
index 0000000..fc075a2
--- /dev/null
@@ -0,0 +1,90 @@
+/*****************************************************************************
+ * vlc_interrupt.h:
+ *****************************************************************************
+ * Copyright (C) 2015 Remlab T:mi
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+/**
+ * @file
+ * This file declares interruptible sleep functions.
+ */
+
+#ifndef VLC_INTERRUPT_H
+# define VLC_INTERRUPT_H 1
+# include <vlc_threads.h>
+/**
+ * @defgroup interrupt Interruptible sleep
+ * @{
+ * @defgroup interrupt_sleep Interruptible sleep functions
+ * @{
+ */
+
+/**
+ * Waits on a semaphore like vlc_sem_wait(). If the calling thread has an
+ * interruption context (as set by vlc_interrupt_set()), and another thread
+ * invokes vlc_interrupt_raise() on that context, the semaphore is incremented.
+ *
+ * @warning The calling thread should be the only thread ever to wait on the
+ * specified semaphore. Otherwise, interruptions may not be delivered
+ * accurately (the wrong thread may be woken up).
+ *
+ * @note This function is (always) a cancellation point.
+ *
+ * @return EINTR if the semaphore was incremented due to an interruption,
+ * otherwise zero.
+ */
+VLC_API int vlc_sem_wait_i11e(vlc_sem_t *);
+
+/**
+ * @}
+ * @defgroup interrupt_context Interrupt context signaling and manipulation
+ * @{
+ */
+typedef struct vlc_interrupt vlc_interrupt_t;
+
+/**
+ * Creates an interruption context.
+ */
+VLC_API vlc_interrupt_t *vlc_interrupt_create(void) VLC_USED;
+
+/**
+ * Destroys an interrupt context.
+ */
+VLC_API void vlc_interrupt_destroy(vlc_interrupt_t *);
+
+/**
+ * Sets the interruption context for the calling thread.
+ * @param newctx the interruption context to attach or NULL for none
+ * @return the previous interruption context or NULL if none
+ *
+ * @note This function is not a cancellation point.
+ * @warning A context can be attached to no more than one thread at a time.
+ */
+VLC_API vlc_interrupt_t *vlc_interrupt_set(vlc_interrupt_t *);
+
+/**
+ * Raises an interruption through a specified context. This is used to
+ * asynchronously wake a thread up while it is waiting on some other events
+ * (typically I/O events).
+ *
+ * @note This function is thread-safe.
+ * @note This function is not a cancellation point.
+ */
+VLC_API void vlc_interrupt_raise(vlc_interrupt_t *);
+
+/** @} @} */
+#endif
index 62c54a6..31d45c4 100644 (file)
@@ -79,6 +79,7 @@ pluginsinclude_HEADERS = \
        ../include/vlc_rand.h \
        ../include/vlc_services_discovery.h \
        ../include/vlc_fingerprinter.h \
+       ../include/vlc_interrupt.h \
        ../include/vlc_sout.h \
        ../include/vlc_spu.h \
        ../include/vlc_stream.h \
@@ -450,6 +451,8 @@ SOURCES_libvlc_common = \
        misc/picture.h \
        misc/picture_fifo.c \
        misc/picture_pool.c \
+       misc/interrupt.h \
+       misc/interrupt.c \
        modules/modules.h \
        modules/modules.c \
        modules/bank.c \
index 73e7dd1..d71e43b 100644 (file)
@@ -539,6 +539,11 @@ vlc_ngettext
 vlc_iconv
 vlc_iconv_close
 vlc_iconv_open
+vlc_sem_wait_i11e
+vlc_interrupt_create
+vlc_interrupt_destroy
+vlc_interrupt_set
+vlc_interrupt_raise
 vlc_join
 vlc_list_children
 vlc_list_release
diff --git a/src/misc/interrupt.c b/src/misc/interrupt.c
new file mode 100644 (file)
index 0000000..ec02601
--- /dev/null
@@ -0,0 +1,233 @@
+/*****************************************************************************
+ * interrupt.c:
+ *****************************************************************************
+ * Copyright (C) 2015 Rémi Denis-Courmont
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+/** @ingroup interrupt */
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include <assert.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdlib.h>
+
+#include <vlc_common.h>
+#include "interrupt.h"
+#include "libvlc.h"
+
+#ifndef NDEBUG
+static void vlc_interrupt_destructor(void *data)
+{
+    vlc_interrupt_t *ctx = data;
+
+    assert(ctx->attached);
+    ctx->attached = false;
+}
+#endif
+
+static unsigned vlc_interrupt_refs = 0;
+static vlc_mutex_t vlc_interrupt_lock = VLC_STATIC_MUTEX;
+static vlc_threadvar_t vlc_interrupt_var;
+
+/**
+ * Initializes an interruption context.
+ */
+void vlc_interrupt_init(vlc_interrupt_t *ctx)
+{
+    vlc_mutex_lock(&vlc_interrupt_lock);
+    assert(vlc_interrupt_refs < UINT_MAX);
+    if (vlc_interrupt_refs++ == 0)
+#ifndef NDEBUG
+        vlc_threadvar_create(&vlc_interrupt_var, vlc_interrupt_destructor);
+#else
+        vlc_threadvar_create(&vlc_interrupt_var, NULL);
+#endif
+    vlc_mutex_unlock(&vlc_interrupt_lock);
+
+    vlc_mutex_init(&ctx->lock);
+    ctx->interrupted = false;
+#ifndef NDEBUG
+    ctx->attached = false;
+#endif
+    ctx->callback = NULL;
+}
+
+vlc_interrupt_t *vlc_interrupt_create(void)
+{
+    vlc_interrupt_t *ctx = malloc(sizeof (*ctx));
+    if (likely(ctx != NULL))
+        vlc_interrupt_init(ctx);
+    return ctx;
+}
+
+/**
+ * Deinitializes an interruption context.
+ * The context shall no longer be used by any thread.
+ */
+void vlc_interrupt_deinit(vlc_interrupt_t *ctx)
+{
+    assert(ctx->callback == NULL);
+    assert(!ctx->attached);
+    vlc_mutex_destroy(&ctx->lock);
+
+    vlc_mutex_lock(&vlc_interrupt_lock);
+    assert(vlc_interrupt_refs > 0);
+    if (--vlc_interrupt_refs == 0)
+        vlc_threadvar_delete(&vlc_interrupt_var);
+    vlc_mutex_unlock(&vlc_interrupt_lock);
+}
+
+void vlc_interrupt_destroy(vlc_interrupt_t *ctx)
+{
+    assert(ctx != NULL);
+    vlc_interrupt_deinit(ctx);
+    free(ctx);
+}
+
+void vlc_interrupt_raise(vlc_interrupt_t *ctx)
+{
+    assert(ctx != NULL);
+
+    /* This function must be reentrant. But the callback typically is not
+     * reentrant. The lock ensures that all calls to the callback for a given
+     * context are serialized. The lock also protects against invalid memory
+     * accesses to the callback pointer proper, and the interrupted flag. */
+    vlc_mutex_lock(&ctx->lock);
+    ctx->interrupted = true;
+    if (ctx->callback != NULL)
+        ctx->callback(ctx->data);
+    vlc_mutex_unlock(&ctx->lock);
+}
+
+vlc_interrupt_t *vlc_interrupt_set(vlc_interrupt_t *newctx)
+{
+    vlc_interrupt_t *oldctx;
+
+    oldctx = vlc_threadvar_get(vlc_interrupt_var);
+#ifndef NDEBUG
+    if (oldctx != NULL)
+    {
+        assert(oldctx->attached);
+        oldctx->attached = false;
+    }
+    if (newctx != NULL)
+    {
+        assert(!newctx->attached);
+        newctx->attached = true;
+    }
+#endif
+    vlc_threadvar_set(vlc_interrupt_var, newctx);
+
+    return oldctx;
+}
+
+/**
+ * Prepares to enter interruptible wait.
+ * @param cb callback to interrupt the wait (i.e. wake up the thread)
+ * @param data opaque data pointer for the callback
+ * @return 0 on success or EINTR if an interruption is already pending
+ * @note Any <b>succesful</b> call <b>must</b> be paired with a call to
+ * vlc_interrupt_finish().
+ */
+static int vlc_interrupt_prepare(vlc_interrupt_t *ctx,
+                                 void (*cb)(void *), void *data)
+{
+    int ret = 0;
+
+    assert(ctx != NULL);
+    assert(ctx == vlc_threadvar_get(vlc_interrupt_var));
+
+    vlc_mutex_lock(&ctx->lock);
+    assert(ctx->callback == NULL);
+    if (ctx->interrupted)
+    {
+        ret = EINTR;
+        ctx->interrupted = false;
+    }
+    else
+    {
+        ret = 0;
+        ctx->callback = cb;
+        ctx->data = data;
+    }
+    vlc_mutex_unlock(&ctx->lock);
+    return ret;
+}
+
+/**
+ * Cleans up after an interruptible wait: waits for any pending invocations of
+ * the callback previously registed with vlc_interrupt_prepare(), and rechecks
+ * for any pending interruption.
+ *
+ * @warning As this function waits for ongoing callback invocation to complete,
+ * the caller must not hold any resource necessary for the callback to run.
+ * Otherwise a deadlock may occur.
+ *
+ * @return EINTR if an interruption occurred, zero otherwise
+ */
+static int vlc_interrupt_finish(vlc_interrupt_t *ctx)
+{
+    int ret = 0;
+
+    assert(ctx != NULL);
+    assert(ctx == vlc_threadvar_get(vlc_interrupt_var));
+
+    /* Wait for pending callbacks to prevent access by other threads. */
+    vlc_mutex_lock(&ctx->lock);
+    ctx->callback = NULL;
+    if (ctx->interrupted)
+    {
+        ret = EINTR;
+        ctx->interrupted = false;
+    }
+    vlc_mutex_unlock(&ctx->lock);
+    return ret;
+}
+
+static void vlc_interrupt_cleanup(void *opaque)
+{
+    vlc_interrupt_finish(opaque);
+}
+
+static void vlc_interrupt_sem(void *opaque)
+{
+    vlc_sem_post(opaque);
+}
+
+int vlc_sem_wait_i11e(vlc_sem_t *sem)
+{
+    vlc_interrupt_t *ctx = vlc_threadvar_get(vlc_interrupt_var);
+    if (ctx == NULL)
+        return vlc_sem_wait(sem), 0;
+
+    int ret = vlc_interrupt_prepare(ctx, vlc_interrupt_sem, sem);
+    if (ret)
+    {
+        vlc_testcancel();
+        return ret;
+    }
+
+    vlc_cleanup_push(vlc_interrupt_cleanup, ctx);
+    vlc_sem_wait(sem);
+    vlc_cleanup_pop();
+
+    return vlc_interrupt_finish(ctx);
+}
diff --git a/src/misc/interrupt.h b/src/misc/interrupt.h
new file mode 100644 (file)
index 0000000..061158a
--- /dev/null
@@ -0,0 +1,40 @@
+/*****************************************************************************
+ * interrupt.h:
+ *****************************************************************************
+ * Copyright (C) 2015 Rémi Denis-Courmont
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+/** @ingroup interrupt */
+#ifndef LIBVLC_INPUT_SIGNAL_H
+# define LIBVLC_INPUT_SIGNAL_H 1
+
+# include <vlc_interrupt.h>
+
+void vlc_interrupt_init(vlc_interrupt_t *);
+void vlc_interrupt_deinit(vlc_interrupt_t *);
+
+struct vlc_interrupt
+{
+    vlc_mutex_t lock;
+    bool interrupted;
+#ifndef NDEBUG
+    bool attached;
+#endif
+    void (*callback)(void *);
+    void *data;
+};
+#endif