Skip to content
Snippets Groups Projects
Commit f20b38e2 authored by Romain Vimont's avatar Romain Vimont
Browse files

qt: medialib: introduce async task

This helper will help implementing asynchronous database queries
initiated on the UI thread by list models.
parent 97f2817c
No related branches found
No related tags found
No related merge requests found
......@@ -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 \
......
/*****************************************************************************
* 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>`).
*/
template <typename T>
class AsyncTaskRunnable;
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<AsyncTaskRunnable<T>>(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:
friend class AsyncTaskRunnable<T>;
std::unique_ptr<AsyncTaskRunnable<T>> 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;
};
/* AsyncTaskRunnable is private (but "public" because template) */
template <typename T>
class AsyncTaskRunnable : public QRunnable
{
public:
AsyncTaskRunnable(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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment