vlc | branch: master | Romain Vimont <[email protected]> | Fri Nov 13 14:04:56 2020 +0100| [2b0783eba45c43cd49e15b23186bfaa1f02fbdad] | committer: Pierre Lamot
qt: medialib: introduce async task This helper will help implementing asynchronous database queries initiated on the UI thread by list models. Signed-off-by: Pierre Lamot <[email protected]> > http://git.videolan.org/gitweb.cgi/vlc.git/?a=commit;h=2b0783eba45c43cd49e15b23186bfaa1f02fbdad --- modules/gui/qt/Makefile.am | 2 + modules/gui/qt/util/asynctask.hpp | 246 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) diff --git a/modules/gui/qt/Makefile.am b/modules/gui/qt/Makefile.am index 985287aa84..35c8e6223d 100644 --- a/modules/gui/qt/Makefile.am +++ b/modules/gui/qt/Makefile.am @@ -197,6 +197,7 @@ libqt_plugin_la_SOURCES = \ gui/qt/playlist/playlist_model.cpp \ gui/qt/playlist/playlist_model.hpp \ gui/qt/playlist/playlist_model_p.hpp \ + gui/qt/util/asynctask.hpp \ gui/qt/util/audio_device_model.cpp \ gui/qt/util/audio_device_model.hpp \ gui/qt/util/color_scheme_model.cpp gui/qt/util/color_scheme_model.hpp \ @@ -340,6 +341,7 @@ nodist_libqt_plugin_la_SOURCES = \ gui/qt/playlist/playlist_controller.moc.cpp \ gui/qt/playlist/playlist_item.moc.cpp \ gui/qt/playlist/playlist_model.moc.cpp \ + gui/qt/util/asynctask.moc.cpp \ gui/qt/util/audio_device_model.moc.cpp \ gui/qt/util/color_scheme_model.moc.cpp \ gui/qt/util/i18n.moc.cpp \ diff --git a/modules/gui/qt/util/asynctask.hpp b/modules/gui/qt/util/asynctask.hpp new file mode 100644 index 0000000000..fa4a6d3b2c --- /dev/null +++ b/modules/gui/qt/util/asynctask.hpp @@ -0,0 +1,246 @@ +/***************************************************************************** + * Copyright (C) 2020 VLC authors and VideoLAN + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU 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_ASYNC_TASK_HPP +#define VLC_ASYNC_TASK_HPP + +#include <atomic> +#include <cassert> +#include <memory> +#include <QMetaObject> +#include <QObject> +#include <QRunnable> +#include <QThreadPool> + +/** + * This class helps executing a processing on a `QThreadPool` and retrieve the + * result on the main thread. + * + * It allows to properly handle the case where the receiver of the result is + * destroyed while a processing is still running, without blocking (provided + * that the `QThreadPool` is not destroyed). + * + * Here is how to use it properly. + * + * First, write a class inheriting `AsyncTask<T>` (`T` being the type of the + * result), and implement the `execute()` method: + * + * \code{.cpp} + * struct MyTask : public AsyncTask<int> + * { + * int m_num; + * + * MyTask(int num) : m_num(num) {} + * + * int execute() override + * { + * // This will be executed from a separate thread + * sleep(1); // long-running task + * return m_num * 2; + * } + * }; + * \endcode + * + * To execute a new task asynchronously, instantiate it, connect the result + * signal and start it: + * + * \code{.cpp} + * // To be called from the main thread + * void SomeQObject::asyncLoad(int num) + * { + * // Create a task and keep a reference in a field + * m_task = new MyTask(num); + * + * // Connect the signal to be notified of the result + * connect(m_task, &BaseAsyncTask::result, + * this, &SomeQObject::onResult); + * + * // Submit the task to some thread pool + * m_start->start(m_threadPool); + * } + * \endcode + * + * The task might be canceled at any time (it might continue running + * asynchronously if it is already running, but the caller will not receive the + * result): + * + * \code{.cpp} + * // To be called from the main thread + * void SomeQObject::cancelCurrentTask() + * { + * if (m_task) + * { + * m_task->abandon(); + * m_task = nullptr; + * } + * } + * \endcode + * + * The result slot must retrieve the result and _abandon_ the task: + * + * \code{.cpp} + * // Called on the main thread + * void SomeQObject::onResult() + * { + * // Retrieve the task instance from the slot, if necessary + * MyTask *task = static_cast<MyTask *>(sender()); + * + * // In basic cases (one task at a time), it should be the same: + * assert(task == m_task); + * + * // Retrieve the result (can only be called once) + * int result = task->takeResult(); + * + * qDebug() << "The result of MyTask(" << task->m_num << ") is" + * << result; + * + * // Abandon the task (so that it can be destroyed) + * task->abandon(); + * m_task = nullptr; + * } + * \endcode + * + * The `abandon()` method allows to handle both cancelation and memory + * management. A task is semantically owned by 2 components: + * - the caller + * - the QThreadPool + * + * Indeed, the caller must be able to _cancel_ it without race conditions, even + * if the task run() has completed. Conversely, the task must not be destroyed + * unilaterally by the caller, to avoid crashing if it is currently running. + * + * The running state is tracked internally, and `abandon()` allows the caller + * to declare that it won't use it anymore (and don't want the result). When + * both conditions are met, the task is deleted via `QObject::deleteLater()`. + * + * The task result is provided via a separate method `takeResult()` instead of + * a slot parameter, because otherwise, Qt would require its type to be: + * - non-template (like `std::vector<T>`), + * - registered to the Qt type system (wrapped into a `QVariant`), + * - copiable (not movable-only, like `std::unique_ptr<T>`). + */ + +class BaseAsyncTask : public QObject +{ + Q_OBJECT + +signals: + void result(); +}; + +template <typename T> +class AsyncTask : public BaseAsyncTask +{ +public: + virtual T execute() = 0; + + /** + * Start the task on the thread pool + */ + void start(QThreadPool &threadPool) + { + m_threadPool = &threadPool; + m_runnable = std::make_unique<Runnable>(this); + threadPool.start(&*m_runnable); + } + + /** + * Abandon the task (cancel and request deletion when possible) + * + * This method must be called from the UI thread. + * + * After this call, the task must not be used and the `result()` signal will + * never be triggered. + */ + void abandon() + { + assert(m_runnable); + assert(m_threadPool); + + bool removed = m_threadPool->tryTake(&*m_runnable); + if (removed) + { + /* run() will never be called */ + deleteLater(); + return; + } + + m_abandoned = true; + if (m_completed) + deleteLater(); + + /* Else it will be deleted from the run() function */ + } + + /** + * Retrieve the result (from the UI thread) + * + * This method consumes the result, and must be called exactly once (from + * the UI thread, in the slot connected to the signal `result()`). + */ + T takeResult() + { + return std::move(m_result); + } + +private: + class Runnable; + std::unique_ptr<Runnable> m_runnable; + + QThreadPool *m_threadPool = nullptr; + + /* Only read and written from the main thread */ + bool m_abandoned = false; + bool m_completed = false; + + T m_result; +}; + +template <typename T> +class AsyncTask<T>::Runnable : public QRunnable +{ +public: + Runnable(AsyncTask<T> *task) : m_task(task) + { + /* The runnable will be owned by the AsyncTask, which needs to keep a + * reference to be able to cancel it using QThreadPool::tryTake(). */ + setAutoDelete(false); + } + + void run() override + { + m_task->m_result = m_task->execute(); + + QMetaObject::invokeMethod(m_task, [task = m_task] + { + /* Check on the main thread to avoid a call to abandon() between the + * call to invokeLater() and the actual execution of the callback */ + task->m_completed = true; + if (task->m_abandoned) + task->deleteLater(); + else + emit task->result(); + }); + } + +private: + friend class AsyncTask<T>; + AsyncTask<T> *m_task; +}; + +#endif _______________________________________________ vlc-commits mailing list [email protected] https://mailman.videolan.org/listinfo/vlc-commits
