Skip to content

Commit c1071b6

Browse files
authored
Merge pull request mixxxdj#16462 from JoergAtGithub/modernizeControllerManager
Modernized ControllerManager to improve thread-safety
2 parents a4ce440 + 1f76d8f commit c1071b6

2 files changed

Lines changed: 95 additions & 48 deletions

File tree

src/controllers/controllermanager.cpp

Lines changed: 74 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,13 @@ ControllerManager::ControllerManager(UserSettingsPointer pConfig)
9393
: QObject(),
9494
m_pConfig(pConfig),
9595
// WARNING: Do not parent m_pControllerLearningEventFilter to
96-
// ControllerManager because the CM is moved to its own thread and runs
97-
// its own event loop.
98-
m_pControllerLearningEventFilter(new ControllerLearningEventFilter()),
96+
// ControllerManager because the CM, together with all its children,
97+
// is moved to the ControllerManager thread (m_pThread) and
98+
// runs its own event loop there.
99+
m_pControllerLearningEventFilter(
100+
std::make_unique<ControllerLearningEventFilter>()),
99101
m_pollTimer(this),
102+
m_pThread(std::make_unique<QThread>()),
100103
m_skipPoll(false) {
101104
qRegisterMetaType<std::shared_ptr<LegacyControllerMapping>>(
102105
"std::shared_ptr<LegacyControllerMapping>");
@@ -111,14 +114,14 @@ ControllerManager::ControllerManager(UserSettingsPointer pConfig)
111114
m_pollTimer.setInterval(kPollInterval.toIntegerMillis());
112115
connect(&m_pollTimer, &QTimer::timeout, this, &ControllerManager::slotPollDevices);
113116

114-
m_pThread = new QThread;
115-
m_pThread->setObjectName("Controller");
117+
m_pThread->setObjectName("ControllerManager");
116118

117-
// Moves all children (including the poll timer) to m_pThread
118-
moveToThread(m_pThread);
119+
// Move the entire ControllerManager object and all its children (including
120+
// the poll timer) onto the ControllerManager thread.
121+
moveToThread(m_pThread.get());
119122

120-
// Controller processing needs to be prioritized since it can affect the
121-
// audio directly, like when scratching
123+
// The ControllerManager thread is high-priority because controller input can
124+
// affect audio output directly (e.g. scratching).
122125
m_pThread->start(QThread::HighPriority);
123126

124127
connect(this, &ControllerManager::requestInitialize, this, &ControllerManager::slotInitialize);
@@ -129,77 +132,96 @@ ControllerManager::ControllerManager(UserSettingsPointer pConfig)
129132
connect(this, &ControllerManager::requestShutdown, this, &ControllerManager::slotShutdown);
130133

131134
// Signal that we should run slotInitialize once our event loop has started
132-
// up.
133-
emit requestInitialize(); // clazy:exclude=incorrect-emit
135+
// up. invokeMethod with QueuedConnection will post an event to our event loop,
136+
// so slotInitialize will not run until after the ControllerManager thread is fully
137+
// started and ready to process events.
138+
QMetaObject::invokeMethod(this, &ControllerManager::slotInitialize, Qt::QueuedConnection);
134139
}
135140

136141
ControllerManager::~ControllerManager() {
137-
emit requestShutdown();
142+
// slotShutdown() closes controllers, deletes enumerators and calls
143+
// m_pThread->quit(). We must wait for the thread to finish before our
144+
// members (m_pollTimer, m_pControllerLearningEventFilter, …) are destroyed,
145+
// because they may still be accessed by the thread's event loop.
146+
emit requestShutdown(); // clazy:exclude=incorrect-emit
138147
m_pThread->wait();
139-
delete m_pThread;
140-
delete m_pControllerLearningEventFilter;
148+
// m_pThread and m_pControllerLearningEventFilter are released by unique_ptr
149+
// after this point, in reverse declaration order.
141150
}
142151

143152
ControllerLearningEventFilter* ControllerManager::getControllerLearningEventFilter() const {
144-
return m_pControllerLearningEventFilter;
153+
return m_pControllerLearningEventFilter.get();
145154
}
146155

147156
void ControllerManager::slotInitialize() {
157+
DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
148158
qDebug() << "ControllerManager:slotInitialize";
149159

150160
// Initialize mapping info parsers. This object is only for use in the main
151161
// thread. Do not touch it from within ControllerManager.
152-
m_pMainThreadUserMappingEnumerator = QSharedPointer<MappingInfoEnumerator>(
153-
new MappingInfoEnumerator(userMappingsPath(m_pConfig)));
154-
m_pMainThreadSystemMappingEnumerator = QSharedPointer<MappingInfoEnumerator>(
155-
new MappingInfoEnumerator(resourceMappingsPath(m_pConfig)));
162+
m_pMainThreadUserMappingEnumerator =
163+
QSharedPointer<MappingInfoEnumerator>::create(
164+
userMappingsPath(m_pConfig));
165+
m_pMainThreadSystemMappingEnumerator =
166+
QSharedPointer<MappingInfoEnumerator>::create(
167+
resourceMappingsPath(m_pConfig));
156168

157169
// Instantiate all enumerators. Enumerators can take a long time to
158170
// construct since they interact with host MIDI APIs.
171+
{
172+
auto locker = lockMutex(&m_mutex);
159173
#ifdef __PORTMIDI__
160-
m_enumerators.append(new PortMidiEnumerator(m_pConfig));
174+
m_enumerators.push_back(std::make_unique<PortMidiEnumerator>(m_pConfig));
161175
#endif
162176
#ifdef __HSS1394__
163-
m_enumerators.append(new Hss1394Enumerator());
177+
m_enumerators.push_back(std::make_unique<Hss1394Enumerator>());
164178
#endif
165179
#ifdef __BULK__
166-
m_enumerators.append(new BulkEnumerator());
180+
m_enumerators.push_back(std::make_unique<BulkEnumerator>());
167181
#endif
168182
#ifdef __HID__
169-
m_enumerators.append(new HidEnumerator());
183+
m_enumerators.push_back(std::make_unique<HidEnumerator>());
170184
#endif
185+
} // Mutex locker released here
171186
emit initialized();
172187
}
173188

174189
void ControllerManager::slotShutdown() {
190+
DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
175191
stopPolling();
176192

177193
// Clear m_enumerators before deleting the enumerators to prevent other code
178-
// paths from accessing them.
194+
// paths from accessing them during teardown.
179195
auto locker = lockMutex(&m_mutex);
180-
QList<ControllerEnumerator*> enumerators = m_enumerators;
181-
m_enumerators.clear();
196+
std::vector<std::unique_ptr<ControllerEnumerator>> enumerators =
197+
std::move(m_enumerators); // m_enumerators is guaranteed empty after move
182198
locker.unlock();
183199

184-
// Delete enumerators and they'll delete their Devices
185-
for (ControllerEnumerator* pEnumerator : enumerators) {
186-
delete pEnumerator;
187-
}
200+
// Delete enumerators (and their owned Controllers) by letting unique_ptrs
201+
// go out of scope here — no raw deletes needed.
202+
enumerators.clear();
188203

189-
// Stop the processor after the enumerators since the engines live in it
204+
// Stop the event loop after the enumerators are torn down, since the
205+
// controller scripting engines live inside the enumerators.
190206
m_pThread->quit();
191207
}
192208

193209
void ControllerManager::updateControllerList() {
210+
DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
194211
// NOTE: Currently this function is only called on startup. If hotplug is added, changes to the
195212
// controller list must be synchronized with dlgprefcontrollers to avoid dangling connections
196213
// and possible crashes.
197214
auto locker = lockMutex(&m_mutex);
198-
if (m_enumerators.isEmpty()) {
215+
if (m_enumerators.empty()) {
199216
qWarning() << "updateControllerList called but no enumerators have been added!";
200217
return;
201218
}
202-
QList<ControllerEnumerator*> enumerators = m_enumerators;
219+
// Take a snapshot of the enumerator pointers while holding the lock.
220+
std::vector<ControllerEnumerator*> enumerators;
221+
enumerators.reserve(m_enumerators.size());
222+
for (const auto& pEnumerator : m_enumerators) {
223+
enumerators.push_back(pEnumerator.get());
224+
}
203225
locker.unlock();
204226

205227
QList<Controller*> newDeviceList;
@@ -208,11 +230,12 @@ void ControllerManager::updateControllerList() {
208230
}
209231

210232
locker.relock();
211-
if (newDeviceList != m_controllers) {
212-
m_controllers = newDeviceList;
213-
locker.unlock();
214-
emit devicesChanged();
233+
if (newDeviceList == m_controllers) {
234+
return;
215235
}
236+
m_controllers = std::move(newDeviceList);
237+
locker.unlock();
238+
emit devicesChanged();
216239
}
217240

218241
QList<Controller*> ControllerManager::getControllers() const {
@@ -240,11 +263,13 @@ QList<Controller*> ControllerManager::getControllerList(bool bOutputDevices, boo
240263
return filteredDeviceList;
241264
}
242265

243-
QString ControllerManager::getConfiguredMappingFileForDevice(const QString& name) {
266+
QString ControllerManager::getConfiguredMappingFileForDevice(const QString& name) const {
267+
// Thread-Safe : ConfigObject<ValueType>::getValueString/get is protected by QWriteLocker
244268
return m_pConfig->getValueString(ConfigKey(kSettingsGroup, sanitizeDeviceName(name)));
245269
}
246270

247271
void ControllerManager::slotSetUpDevices() {
272+
DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
248273
qDebug() << "ControllerManager: Setting up devices";
249274

250275
updateControllerList();
@@ -311,6 +336,8 @@ void ControllerManager::slotSetUpDevices() {
311336
}
312337

313338
void ControllerManager::pollIfAnyControllersOpen() {
339+
// Not Thread-Safe because calls startPolling()/stopPolling()
340+
DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
314341
auto locker = lockMutex(&m_mutex);
315342
QList<Controller*> controllers = m_controllers;
316343
locker.unlock();
@@ -329,6 +356,8 @@ void ControllerManager::pollIfAnyControllersOpen() {
329356
}
330357

331358
void ControllerManager::startPolling() {
359+
// Not Thread-Safe because QTimer::start() must be called from the thread that owns the timer.
360+
DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
332361
// Start the polling timer.
333362
if (!m_pollTimer.isActive()) {
334363
m_pollTimer.start();
@@ -337,11 +366,15 @@ void ControllerManager::startPolling() {
337366
}
338367

339368
void ControllerManager::stopPolling() {
369+
// Not Thread-Safe because QTimer::stop() must be called from the thread that owns the timer.
370+
DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
340371
m_pollTimer.stop();
341372
qDebug() << "Controller polling stopped.";
342373
}
343374

344375
void ControllerManager::slotPollDevices() {
376+
// Not Thread-Safe because it accesses m_controllers and m_skipPoll without a mutex.
377+
DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
345378
// Note: this function is called from a high priority thread which
346379
// may stall the GUI or may reduce the available CPU time for other
347380
// High Priority threads like caching reader or broadcasting more
@@ -384,7 +417,7 @@ void ControllerManager::slotPollDevices() {
384417
}
385418

386419
void ControllerManager::openController(Controller* pController) {
387-
DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
420+
DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
388421
if (!pController) {
389422
return;
390423
}
@@ -404,7 +437,7 @@ void ControllerManager::openController(Controller* pController) {
404437
}
405438

406439
void ControllerManager::closeController(Controller* pController) {
407-
DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
440+
DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
408441
if (!pController) {
409442
return;
410443
}
@@ -420,6 +453,7 @@ void ControllerManager::closeController(Controller* pController) {
420453
void ControllerManager::slotApplyMapping(Controller* pController,
421454
std::shared_ptr<LegacyControllerMapping> pMapping,
422455
bool bEnabled) {
456+
DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
423457
VERIFY_OR_DEBUG_ASSERT(pController) {
424458
qWarning() << "slotApplyMapping got invalid controller!";
425459
return;

src/controllers/controllermanager.h

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <QSharedPointer>
55
#include <QTimer>
66
#include <memory>
7+
#include <vector>
78

89
#include "controllers/controllerenumerator.h"
910
#include "preferences/usersettings.h"
@@ -23,21 +24,21 @@ bool controllerCompare(Controller *a, Controller *b);
2324
class ControllerManager : public QObject {
2425
Q_OBJECT
2526
public:
26-
ControllerManager(UserSettingsPointer pConfig);
27-
virtual ~ControllerManager();
27+
explicit ControllerManager(UserSettingsPointer pConfig);
28+
~ControllerManager() override;
2829

2930
static const mixxx::Duration kPollInterval;
3031

3132
QList<Controller*> getControllers() const;
3233
QList<Controller*> getControllerList(bool outputDevices=true, bool inputDevices=true);
3334
ControllerLearningEventFilter* getControllerLearningEventFilter() const;
34-
QSharedPointer<MappingInfoEnumerator> getMainThreadUserMappingEnumerator() {
35+
QSharedPointer<MappingInfoEnumerator> getMainThreadUserMappingEnumerator() const {
3536
return m_pMainThreadUserMappingEnumerator;
3637
}
37-
QSharedPointer<MappingInfoEnumerator> getMainThreadSystemMappingEnumerator() {
38+
QSharedPointer<MappingInfoEnumerator> getMainThreadSystemMappingEnumerator() const {
3839
return m_pMainThreadSystemMappingEnumerator;
3940
}
40-
QString getConfiguredMappingFileForDevice(const QString& name);
41+
QString getConfiguredMappingFileForDevice(const QString& name) const;
4142

4243
/// Prevent other parts of Mixxx from having to manually connect to our slots
4344
void setUpDevices() { emit requestSetUpDevices(); };
@@ -78,13 +79,25 @@ class ControllerManager : public QObject {
7879
void closeController(Controller* pController);
7980

8081
UserSettingsPointer m_pConfig;
81-
ControllerLearningEventFilter* m_pControllerLearningEventFilter;
82+
// WARNING: Do not parent m_pControllerLearningEventFilter to ControllerManager
83+
// because the CM is moved to its own thread and runs its own event loop.
84+
std::unique_ptr<ControllerLearningEventFilter> m_pControllerLearningEventFilter;
8285
QTimer m_pollTimer;
8386
mutable QMutex m_mutex;
84-
QList<ControllerEnumerator*> m_enumerators;
87+
/// Guarded by m_mutex for main-thread reads.
88+
/// Written/iterated only on the ControllerManager thread
89+
std::vector<std::unique_ptr<ControllerEnumerator>> m_enumerators;
90+
/// Non-owning; Controllers are owned by their respective ControllerEnumerator.
91+
/// Guarded by m_mutex for main-thread reads.
92+
/// Written only on the ControllerManager thread
8593
QList<Controller*> m_controllers;
86-
QThread* m_pThread;
94+
/// The single shared background thread that drives the entire ControllerManager,
95+
/// all ControllerEnumerators, and all Controller instances.
96+
std::unique_ptr<QThread> m_pThread;
97+
/// Written once on the ControllerManager thread during slotInitialize(),
98+
/// before initialized() is emitted. Afterwards only read from the main thread
8799
QSharedPointer<MappingInfoEnumerator> m_pMainThreadUserMappingEnumerator;
88100
QSharedPointer<MappingInfoEnumerator> m_pMainThreadSystemMappingEnumerator;
101+
/// Accessed only from the ControllerManager thread via slotPollDevices().
89102
bool m_skipPoll;
90103
};

0 commit comments

Comments
 (0)