Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
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