Add a safer way to use QThreadPool::reserveThread

Add startOnReservedThread that specifically releases a reserved thread
and uses it atomically for a given task. This can make a positive
number of reserved threads work.

Change-Id: I4bd1dced24bb46fcb365f12cbc9c7905dc66cdf1
Reviewed-by: David Faure <david.faure@kdab.com>
This commit is contained in:
Allan Sandfeld Jensen 2021-05-10 17:34:20 +02:00
parent 2c3617dfcb
commit 733923c81c
3 changed files with 106 additions and 2 deletions

View File

@ -770,6 +770,57 @@ void QThreadPool::releaseThread()
d->tryToStartMoreThreads();
}
/*!
Releases a thread previously reserved with reserveThread() and uses it
to run \a runnable.
Note that the thread pool takes ownership of the \a runnable if
\l{QRunnable::autoDelete()}{runnable->autoDelete()} returns \c true,
and the \a runnable will be deleted automatically by the thread
pool after the \l{QRunnable::run()}{runnable->run()} returns. If
\l{QRunnable::autoDelete()}{runnable->autoDelete()} returns \c false,
ownership of \a runnable remains with the caller. Note that
changing the auto-deletion on \a runnable after calling this
functions results in undefined behavior.
\note Calling this when no threads are reserved results in
undefined behavior.
\since 6.3
\sa reserveThread(), start()
*/
void QThreadPool::startOnReservedThread(QRunnable *runnable)
{
if (!runnable)
return releaseThread();
Q_D(QThreadPool);
QMutexLocker locker(&d->mutex);
Q_ASSERT(d->reservedThreads > 0);
--d->reservedThreads;
if (!d->tryStart(runnable)) {
// This can only happen if we reserved max threads,
// and something took the one minimum thread.
d->enqueueTask(runnable, INT_MAX);
}
}
/*!
\overload
\since 6.3
Releases a thread previously reserved with reserveThread() and uses it
to run \a functionToRun.
*/
void QThreadPool::startOnReservedThread(std::function<void()> functionToRun)
{
if (!functionToRun)
return releaseThread();
startOnReservedThread(QRunnable::create(std::move(functionToRun)));
}
/*!
Waits up to \a msecs milliseconds for all threads to exit and removes all
threads from the thread pool. Returns \c true if all threads were removed;

View File

@ -75,6 +75,9 @@ public:
void start(std::function<void()> functionToRun, int priority = 0);
bool tryStart(std::function<void()> functionToRun);
void startOnReservedThread(QRunnable *runnable);
void startOnReservedThread(std::function<void()> functionToRun);
int expiryTimeout() const;
void setExpiryTimeout(int expiryTimeout);

View File

@ -89,6 +89,7 @@ private slots:
void releaseThread_data();
void releaseThread();
void reserveAndStart();
void reserveAndStart2();
void releaseAndBlock();
void start();
void tryStart();
@ -729,17 +730,66 @@ void tst_QThreadPool::reserveAndStart() // QTBUG-21051
// start() will wake up the waiting thread.
threadpool->start(&task);
QTRY_COMPARE(threadpool->activeThreadCount(), 2);
QTRY_COMPARE(task.count.loadRelaxed(), 2);
WaitingTask task2;
// startOnReservedThread() will try to take the reserved task, but end up waiting instead
threadpool->startOnReservedThread(&task2);
QTRY_COMPARE(threadpool->activeThreadCount(), 1);
task.waitForStarted.acquire();
task.waitBeforeDone.release();
QTRY_COMPARE(task.count.loadRelaxed(), 2);
QTRY_COMPARE(threadpool->activeThreadCount(), 1);
task2.waitForStarted.acquire();
task2.waitBeforeDone.release();
threadpool->releaseThread();
QTRY_COMPARE(threadpool->activeThreadCount(), 0);
threadpool->setMaxThreadCount(savedLimit);
}
void tst_QThreadPool::reserveAndStart2()
{
class WaitingTask : public QRunnable
{
public:
QSemaphore waitBeforeDone;
WaitingTask() { setAutoDelete(false); }
void run() override
{
waitBeforeDone.acquire();
}
};
// Set up
QThreadPool *threadpool = QThreadPool::globalInstance();
int savedLimit = threadpool->maxThreadCount();
threadpool->setMaxThreadCount(2);
// reserve
threadpool->reserveThread();
// start two task, to get a running thread and one queued
WaitingTask task1, task2, task3;
threadpool->start(&task1);
// one running thread, one reserved:
QCOMPARE(threadpool->activeThreadCount(), 2);
// task2 starts queued
threadpool->start(&task2);
QCOMPARE(threadpool->activeThreadCount(), 2);
// startOnReservedThread() will take the reserved thread however, bypassing the queue
threadpool->startOnReservedThread(&task3);
// two running threads, none reserved:
QCOMPARE(threadpool->activeThreadCount(), 2);
task3.waitBeforeDone.release();
// task3 can finish even if all other tasks are blocking
// then task2 will use the previously reserved thread
task2.waitBeforeDone.release();
QTRY_COMPARE(threadpool->activeThreadCount(), 1);
task1.waitBeforeDone.release();
QTRY_COMPARE(threadpool->activeThreadCount(), 0);
}
void tst_QThreadPool::releaseAndBlock()
{
class WaitingTask : public QRunnable