[vlc-commits] executor: introduce new executor API
Romain Vimont
git at videolan.org
Fri Oct 23 17:48:02 CEST 2020
vlc | branch: master | Romain Vimont <rom1v at videolabs.io> | Tue Sep 1 18:09:09 2020 +0200| [fb48df6fdf2d784e92f050be371281f872dc87b9] | committer: Alexandre Janniaux
executor: introduce new executor API
Introduce a new API to execute "runnables" from background threads.
The final design is a result of discussions on the mailing-list:
- <https://mailman.videolan.org/pipermail/vlc-devel/2020-August/136696.html>
- <https://mailman.videolan.org/pipermail/vlc-devel/2020-September/136944.html>
Signed-off-by: Alexandre Janniaux <ajanni at videolabs.io>
> http://git.videolan.org/gitweb.cgi/vlc.git/?a=commit;h=fb48df6fdf2d784e92f050be371281f872dc87b9
---
include/vlc_executor.h | 183 +++++++++++++++++++++++++++++++
src/Makefile.am | 2 +
src/libvlccore.sym | 5 +
src/misc/executor.c | 288 +++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 478 insertions(+)
diff --git a/include/vlc_executor.h b/include/vlc_executor.h
new file mode 100644
index 0000000000..2d38bdb95c
--- /dev/null
+++ b/include/vlc_executor.h
@@ -0,0 +1,183 @@
+/*****************************************************************************
+ * vlc_executor.h
+ *****************************************************************************
+ * Copyright (C) 2020 Videolabs, VLC authors and VideoLAN
+ *
+ * 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.
+ *****************************************************************************/
+
+#ifndef VLC_EXECUTOR_H
+#define VLC_EXECUTOR_H
+
+#include <vlc_common.h>
+#include <vlc_list.h>
+
+# ifdef __cplusplus
+extern "C" {
+# endif
+
+/** Executor type (opaque) */
+typedef struct vlc_executor vlc_executor_t;
+
+/**
+ * A Runnable encapsulates a task to be run from an executor thread.
+ */
+struct vlc_runnable {
+
+ /**
+ * This function is to be executed by a vlc_executor_t.
+ *
+ * It must implement the actions (arbitrarily long) to execute from an
+ * executor thread, synchronously. As soon as run() returns, the execution
+ * of this runnable is complete.
+ *
+ * After the runnable is submitted to an executor via
+ * vlc_executor_Submit(), the run() function is executed at most once (zero
+ * if the execution is canceled before it was started).
+ *
+ * It must not be NULL.
+ *
+ * \param userdata the userdata provided to vlc_executor_Submit()
+ */
+ void (*run)(void *userdata);
+
+ /**
+ * Userdata passed back to run().
+ */
+ void *userdata;
+
+ /* Private data used by the vlc_executor_t (do not touch) */
+ struct vlc_list node;
+};
+
+/**
+ * Create a new executor.
+ *
+ * \param max_threads the maximum number of threads used to execute runnables
+ * \return a pointer to a new executor, or NULL if an error occurred
+ */
+VLC_API vlc_executor_t *
+vlc_executor_New(unsigned max_threads);
+
+/**
+ * Delete an executor.
+ *
+ * Wait for all the threads to complete, and delete the executor instance.
+ *
+ * All submitted tasks must be either started or explicitly canceled. To wait
+ * for all tasks to complete, use vlc_executor_WaitIdle().
+ *
+ * It is an error to submit a new runnable after vlc_executor_Delete() is
+ * called. In particular, a running task must not submit a new runnable once
+ * deletion has been requested.
+ *
+ * \param executor the executor
+ */
+VLC_API void
+vlc_executor_Delete(vlc_executor_t *executor);
+
+/**
+ * Submit a runnable for execution.
+ *
+ * The struct vlc_runnable is not copied, it must exist until the end of the
+ * execution (the user is expected to embed it in its own task structure).
+ *
+ * Here is a simple example:
+ *
+ * \code{c}
+ * struct my_task {
+ * char *str;
+ * struct vlc_runnable runnable;
+ * };
+ *
+ * static void Run(void *userdata)
+ * {
+ * struct my_task *task = userdata;
+ *
+ * printf("start of %s\n", task->str);
+ * vlc_tick_sleep(VLC_TICK_FROM_SEC(3)); // long action
+ * printf("end of %s\n", task->str);
+ *
+ * free(task->str);
+ * free(task);
+ * }
+ *
+ * void foo(vlc_executor_t *executor, const char *str)
+ * {
+ * // no error handling for brevity
+ * struct my_task *task = malloc(sizeof(*task));
+ * task->str = strdup(str);
+ * task->runnable.run = Run;
+ * task->runnable.userdata = task;
+ * vlc_executor_Submit(executor, &task->runnable);
+ * }
+ * \endcode
+ *
+ * A runnable instance is intended to be submitted at most once. The caller is
+ * expected to allocate a new task structure (embedding the runnable) for every
+ * submission.
+ *
+ * More precisely, it is incorrect to submit a runnable already submitted that
+ * is still in the pending queue (i.e. not canceled or started). This is due to
+ * the intrusive linked list of runnables.
+ *
+ * It is strongly discouraged to submit a runnable that is currently running on
+ * the executor (unless you are prepared for the run() callback to be run
+ * several times in parallel).
+ *
+ * For simplicity, it is discouraged to submit a runnable previously submitted.
+ *
+ * \param executor the executor
+ * \param runnable the task to run
+ */
+VLC_API void
+vlc_executor_Submit(vlc_executor_t *executor, struct vlc_runnable *runnable);
+
+/**
+ * Cancel a runnable previously submitted.
+ *
+ * If this runnable is still queued (i.e. it has not be run yet), then dequeue
+ * it so that it will never be run, and return true.
+ *
+ * Otherwise, this runnable has already been taken by an executor thread (it is
+ * still running or is complete). In that case, do nothing, and return false.
+ *
+ * This is an error to pass a runnable not submitted to this executor (the
+ * result is undefined in that case).
+ *
+ * Note that the runnable instance is owned by the caller, so the executor will
+ * never attempt to free it.
+ *
+ * \param executor the executor
+ * \param runnable the task to cancel
+ * \retval true if the runnable has been canceled before execution
+ * \retval false if the runnable has not been canceled
+ */
+VLC_API bool
+vlc_executor_Cancel(vlc_executor_t *executor, struct vlc_runnable *runnable);
+
+/**
+ * Wait until all submitted tasks are completed or canceled.
+ *
+ * \param executor the executor
+ */
+VLC_API void
+vlc_executor_WaitIdle(vlc_executor_t *executor);
+
+# ifdef __cplusplus
+}
+# endif
+
+ #endif
diff --git a/src/Makefile.am b/src/Makefile.am
index bd4136dd8f..5b240edbab 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -49,6 +49,7 @@ pluginsinclude_HEADERS = \
../include/vlc_es.h \
../include/vlc_es_out.h \
../include/vlc_events.h \
+ ../include/vlc_executor.h \
../include/vlc_filter.h \
../include/vlc_fingerprinter.h \
../include/vlc_fourcc.h \
@@ -353,6 +354,7 @@ libvlccore_la_SOURCES = \
misc/actions.c \
misc/background_worker.c \
misc/background_worker.h \
+ misc/executor.c \
misc/md5.c \
misc/probe.c \
misc/rand.c \
diff --git a/src/libvlccore.sym b/src/libvlccore.sym
index cd47af0a2e..c4d5273dbd 100644
--- a/src/libvlccore.sym
+++ b/src/libvlccore.sym
@@ -976,3 +976,8 @@ vlc_video_context_GetType
vlc_video_context_GetPrivate
vlc_video_context_Hold
vlc_video_context_HoldDevice
+vlc_executor_New
+vlc_executor_Delete
+vlc_executor_Submit
+vlc_executor_Cancel
+vlc_executor_WaitIdle
diff --git a/src/misc/executor.c b/src/misc/executor.c
new file mode 100644
index 0000000000..3b79f9ba8d
--- /dev/null
+++ b/src/misc/executor.c
@@ -0,0 +1,288 @@
+/*****************************************************************************
+ * misc/executor.c
+ *****************************************************************************
+ * Copyright (C) 2020 Videolabs, VLC authors and VideoLAN
+ *
+ * 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.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <vlc_executor.h>
+
+#include <vlc_atomic.h>
+#include <vlc_list.h>
+#include <vlc_threads.h>
+#include "libvlc.h"
+
+/**
+ * An executor can spawn several threads.
+ *
+ * This structure contains the data specific to one thread.
+ */
+struct vlc_executor_thread {
+ /** Node of vlc_executor.threads list */
+ struct vlc_list node;
+
+ /** The executor owning the thread */
+ vlc_executor_t *owner;
+
+ /** The system thread */
+ vlc_thread_t thread;
+
+ /** The current task executed by the thread, NULL if none */
+ struct vlc_runnable *current_task;
+};
+
+/**
+ * The executor (also vlc_executor_t, exposed as opaque type in the public
+ * header).
+ */
+struct vlc_executor {
+ vlc_mutex_t lock;
+
+ /** Maximum number of threads to run the tasks */
+ unsigned max_threads;
+
+ /** List of active vlc_executor_thread */
+ struct vlc_list threads;
+
+ /** Thread count (in a separate field to quickly compare to max_threads) */
+ unsigned nthreads;
+
+ /* Number of tasks requested but not finished. */
+ unsigned unfinished;
+
+ /** Wait for the executor to be idle (i.e. unfinished == 0) */
+ vlc_cond_t idle_wait;
+
+ /** Queue of vlc_runnable */
+ struct vlc_list queue;
+
+ /** Wait for the queue to be non-empty */
+ vlc_cond_t queue_wait;
+
+ /** True if executor deletion is requested */
+ bool closing;
+};
+
+static void
+QueuePush(vlc_executor_t *executor, struct vlc_runnable *runnable)
+{
+ vlc_mutex_assert(&executor->lock);
+
+ vlc_list_append(&runnable->node, &executor->queue);
+ vlc_cond_signal(&executor->queue_wait);
+}
+
+static struct vlc_runnable *
+QueueTake(vlc_executor_t *executor)
+{
+ vlc_mutex_assert(&executor->lock);
+
+ while (!executor->closing && vlc_list_is_empty(&executor->queue))
+ vlc_cond_wait(&executor->queue_wait, &executor->lock);
+
+ if (executor->closing)
+ return NULL;
+
+ struct vlc_runnable *runnable =
+ vlc_list_first_entry_or_null(&executor->queue, struct vlc_runnable,
+ node);
+ assert(runnable);
+ vlc_list_remove(&runnable->node);
+
+ /* Set links to NULL to know that it has been taken by a thread in
+ * vlc_executor_Cancel() */
+ runnable->node.prev = runnable->node.next = NULL;
+
+ return runnable;
+}
+
+static void *
+ThreadRun(void *userdata)
+{
+ struct vlc_executor_thread *thread = userdata;
+ vlc_executor_t *executor = thread->owner;
+
+ vlc_mutex_lock(&executor->lock);
+
+ struct vlc_runnable *runnable;
+ /* When the executor is closing, QueueTake() returns NULL */
+ while ((runnable = QueueTake(executor)))
+ {
+ thread->current_task = runnable;
+ vlc_mutex_unlock(&executor->lock);
+
+ /* Execute the user-provided runnable, without the executor lock */
+ runnable->run(runnable->userdata);
+
+ vlc_mutex_lock(&executor->lock);
+ thread->current_task = NULL;
+
+ assert(executor->unfinished > 0);
+ --executor->unfinished;
+ if (!executor->unfinished)
+ vlc_cond_signal(&executor->idle_wait);
+ }
+
+ vlc_mutex_unlock(&executor->lock);
+
+ return NULL;
+}
+
+static int
+SpawnThread(vlc_executor_t *executor)
+{
+ assert(executor->nthreads < executor->max_threads);
+
+ struct vlc_executor_thread *thread = malloc(sizeof(*thread));
+ if (!thread)
+ return VLC_ENOMEM;
+
+ thread->owner = executor;
+ thread->current_task = NULL;
+
+ if (vlc_clone(&thread->thread, ThreadRun, thread, VLC_THREAD_PRIORITY_LOW))
+ {
+ free(thread);
+ return VLC_EGENERIC;
+ }
+
+ executor->nthreads++;
+ vlc_list_append(&thread->node, &executor->threads);
+
+ return VLC_SUCCESS;
+}
+
+vlc_executor_t *
+vlc_executor_New(unsigned max_threads)
+{
+ assert(max_threads);
+ vlc_executor_t *executor = malloc(sizeof(*executor));
+ if (!executor)
+ return NULL;
+
+ vlc_mutex_init(&executor->lock);
+
+ executor->max_threads = max_threads;
+ executor->nthreads = 0;
+ executor->unfinished = 0;
+
+ vlc_list_init(&executor->threads);
+ vlc_list_init(&executor->queue);
+
+ vlc_cond_init(&executor->idle_wait);
+ vlc_cond_init(&executor->queue_wait);
+
+ executor->closing = false;
+
+ /* Create one thread on init so that vlc_executor_Submit() may never fail */
+ int ret = SpawnThread(executor);
+ if (ret != VLC_SUCCESS)
+ {
+ free(executor);
+ return NULL;
+ }
+
+ return executor;
+}
+
+void
+vlc_executor_Submit(vlc_executor_t *executor, struct vlc_runnable *runnable)
+{
+ vlc_mutex_lock(&executor->lock);
+
+ assert(!executor->closing);
+
+ QueuePush(executor, runnable);
+
+ if (++executor->unfinished > executor->nthreads
+ && executor->nthreads < executor->max_threads)
+ /* If it fails, this is not an error, there is at least one thread */
+ SpawnThread(executor);
+
+ vlc_mutex_unlock(&executor->lock);
+}
+
+bool
+vlc_executor_Cancel(vlc_executor_t *executor, struct vlc_runnable *runnable)
+{
+ vlc_mutex_lock(&executor->lock);
+
+ /* Either both prev and next are set, either both are NULL */
+ assert(!runnable->node.prev == !runnable->node.next);
+
+ bool in_queue = runnable->node.prev;
+ if (in_queue)
+ {
+ vlc_list_remove(&runnable->node);
+
+ assert(executor->unfinished > 0);
+ --executor->unfinished;
+ if (!executor->unfinished)
+ vlc_cond_signal(&executor->idle_wait);
+ }
+
+ vlc_mutex_unlock(&executor->lock);
+
+ return in_queue;
+}
+
+void
+vlc_executor_WaitIdle(vlc_executor_t *executor)
+{
+ vlc_mutex_lock(&executor->lock);
+ while (executor->unfinished)
+ vlc_cond_wait(&executor->idle_wait, &executor->lock);
+ vlc_mutex_unlock(&executor->lock);
+}
+
+void
+vlc_executor_Delete(vlc_executor_t *executor)
+{
+ vlc_mutex_lock(&executor->lock);
+
+ executor->closing = true;
+
+ /* All the tasks must be canceled on delete */
+ assert(vlc_list_is_empty(&executor->queue));
+
+ vlc_mutex_unlock(&executor->lock);
+
+ /* "closing" is now true, this will wake up threads */
+ vlc_cond_broadcast(&executor->queue_wait);
+
+ /* The threads list may not be written at this point, so it is safe to read
+ * it without mutex locked (the mutex must be released to join the
+ * threads). */
+
+ struct vlc_executor_thread *thread;
+ vlc_list_foreach(thread, &executor->threads, node)
+ {
+ vlc_join(thread->thread, NULL);
+ free(thread);
+ }
+
+ /* The queue must still be empty (no runnable submitted a new runnable) */
+ assert(vlc_list_is_empty(&executor->queue));
+
+ /* There are no tasks anymore */
+ assert(!executor->unfinished);
+
+ free(executor);
+}
More information about the vlc-commits
mailing list